Skip to content

Commit 95982b3

Browse files
committed
feat(explore): implement ExplorePageClient for resource filtering and pagination
1 parent 21926f0 commit 95982b3

File tree

3 files changed

+90
-31
lines changed

3 files changed

+90
-31
lines changed

src/actions/resources.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,47 @@
33
import { revalidatePath } from "next/cache";
44

55
import { createClient } from "@/lib/supabase/server";
6+
import type { Resource, ResourceList, ResourceForm, Filters } from "@/types";
67

7-
import type { Resource, ResourceList, ResourceForm } from "@/types";
8-
9-
export const getResources = async (): Promise<ResourceList[]> => {
8+
export async function getResources(
9+
filters: Filters,
10+
from: number = 0,
11+
to: number = 9,
12+
): Promise<Resource[]> {
1013
const supabase = await createClient();
11-
const { data, error } = await supabase.from("resources").select(`
12-
id,
13-
name,
14-
slug,
15-
image,
16-
tags,
17-
category:categories (id, name, slug, created_at)
18-
`);
14+
let query = supabase.from("resources").select("*").range(from, to);
1915

20-
if (error) throw new Error(error.message);
21-
// @ts-expect-error - supabase returns an array, but we want to return a single object
16+
// Filters
17+
if (filters.category) {
18+
query = query.eq("category", filters.category);
19+
}
20+
21+
if (filters.search) {
22+
query = query.ilike("name", `%${filters.search}%`);
23+
}
24+
25+
// Sorting
26+
switch (filters.sortBy) {
27+
case "newest":
28+
query = query.order("created_at", { ascending: false });
29+
break;
30+
case "oldest":
31+
query = query.order("created_at", { ascending: true });
32+
break;
33+
case "popular":
34+
query = query.order("views", { ascending: false });
35+
break;
36+
case "name":
37+
query = query.order("name", { ascending: true });
38+
break;
39+
case "":
40+
break;
41+
}
42+
43+
const { data, error } = await query;
44+
if (error) throw new Error(`Failed to fetch resources: ${error.message}`);
2245
return data;
23-
};
46+
}
2447

2548
export const getAllResources = async (): Promise<Resource[]> => {
2649
const supabase = await createClient();

src/app/(public)/explore/client.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"use client";
2+
3+
import { useCallback } from "react";
4+
import { useSearchParams } from "next/navigation";
5+
import { Skeleton } from "@heroui/react";
6+
7+
import { getResources } from "@/actions/resources";
8+
import { EmptyList } from "@/components/layout";
9+
import { ResourceCard } from "@/components/resources";
10+
import { InfiniteList } from "@/components/ui";
11+
import type { Filters } from "@/types";
12+
13+
export default function ExplorePageClient() {
14+
const searchParams = useSearchParams();
15+
16+
// Functions
17+
const getCurrentFilters = useCallback((): Filters => {
18+
return {
19+
category: searchParams.get("category") || "",
20+
sortBy: (searchParams.get("sortBy") as Filters["sortBy"]) || "",
21+
search: searchParams.get("search") || "",
22+
};
23+
}, [searchParams]);
24+
25+
return (
26+
<InfiniteList
27+
pageSize={3}
28+
loadMore={(from, to) => getResources(getCurrentFilters(), from, to)}
29+
renderItem={(resource) => (
30+
<ResourceCard key={resource.id} resource={resource} />
31+
)}
32+
renderWrapper={(children) => (
33+
<section className="grid grid-cols-1 md:grid-cols-3 gap-4">
34+
{children}
35+
</section>
36+
)}
37+
renderEmpty={() => <EmptyList type="resources" />}
38+
renderSkeleton={() => (
39+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
40+
{[...Array(3)].map((_, idx) => (
41+
<Skeleton key={idx} className="h-60 rounded-xl" />
42+
))}
43+
</div>
44+
)}
45+
/>
46+
);
47+
}

src/app/(public)/explore/page.tsx

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
1-
import type { Metadata } from "next";
2-
3-
import { getRandomResources } from "@/actions/resources";
4-
import { ResourceList } from "@/components/resources";
5-
import { EmptyList } from "@/components/layout";
6-
import { Heading } from "@/components/ui";
1+
import { FilterBar, Heading } from "@/components/ui";
72
import { PROJECT_NAME } from "@/config/constants";
8-
9-
export const revalidate = 86400; // 24 hours
3+
import ExplorePageClient from "./client";
4+
import type { Metadata } from "next";
105

116
export const metadata: Metadata = {
127
title: `Explore - ${PROJECT_NAME}`,
138
description: "Discover new resources",
149
};
1510

16-
export default async function ExplorePage() {
17-
const resources = await getRandomResources();
18-
19-
const renderResources = () => {
20-
if (resources.length === 0) return <EmptyList type="resources" />;
21-
return <ResourceList resources={resources} />;
22-
};
23-
11+
export default function ExplorePage() {
2412
return (
2513
<main className="max-w-screen-xl mx-auto p-4 flex flex-col gap-4">
2614
<Heading title="Explore" subtitle="Discover new resources" />
27-
{renderResources()}
15+
<FilterBar />
16+
<ExplorePageClient />
2817
</main>
2918
);
3019
}

0 commit comments

Comments
 (0)