Skip to content

Commit f39d22a

Browse files
committed
Explore Page Created
1 parent 99b2d0b commit f39d22a

File tree

2 files changed

+241
-0
lines changed

2 files changed

+241
-0
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// src/app/courses/ExploreCoursesClient.tsx
2+
"use client";
3+
4+
import { useMemo, useState } from "react";
5+
import Link from "next/link";
6+
7+
import {
8+
Card,
9+
CardHeader,
10+
CardTitle,
11+
CardDescription,
12+
CardContent,
13+
} from "@/components/ui/card";
14+
import { Badge } from "@/components/ui/badge";
15+
import { Input } from "@/components/ui/input";
16+
import { Button } from "@/components/ui/button";
17+
import {
18+
Tabs,
19+
TabsList,
20+
TabsTrigger,
21+
TabsContent,
22+
} from "@/components/ui/tabs";
23+
24+
type CourseItem = {
25+
id: number | string;
26+
title: string;
27+
description: string;
28+
difficulty: string | null;
29+
createdAt: number;
30+
};
31+
32+
type ExploreCoursesClientProps = {
33+
courses: CourseItem[];
34+
inProgressCourseIds: Array<number | string>;
35+
};
36+
37+
const PAGE_SIZE = 10;
38+
39+
export function ExploreCoursesClient({
40+
courses,
41+
inProgressCourseIds,
42+
}: ExploreCoursesClientProps) {
43+
const [search, setSearch] = useState("");
44+
const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
45+
46+
const normalizedSearch = search.trim().toLowerCase();
47+
48+
const filteredCourses = useMemo(() => {
49+
if (!normalizedSearch) return courses;
50+
return courses.filter((course) => {
51+
const haystack = `${course.title} ${course.description}`.toLowerCase();
52+
return haystack.includes(normalizedSearch);
53+
});
54+
}, [courses, normalizedSearch]);
55+
56+
const popularCourses = useMemo(() => {
57+
return filteredCourses.slice(0, 5);
58+
}, [filteredCourses]);
59+
60+
const inProgressCourses = useMemo(() => {
61+
if (!inProgressCourseIds.length) return [];
62+
const set = new Set(inProgressCourseIds.map(String));
63+
return filteredCourses.filter((c) => set.has(String(c.id)));
64+
}, [filteredCourses, inProgressCourseIds]);
65+
66+
const visibleCourses = filteredCourses.slice(0, visibleCount);
67+
const hasMore = filteredCourses.length > visibleCourses.length;
68+
69+
const renderCourseCard = (course: CourseItem) => {
70+
const difficultyLabel =
71+
course.difficulty?.charAt(0).toUpperCase() +
72+
course.difficulty?.slice(1) || "Beginner";
73+
74+
const date =
75+
course.createdAt && !Number.isNaN(course.createdAt)
76+
? new Date(course.createdAt * 1000)
77+
: null;
78+
79+
return (
80+
<Link key={course.id} href={`/courses/${course.id}`}>
81+
<Card className="h-full transition hover:border-primary/60 hover:shadow-sm">
82+
<CardHeader>
83+
<div className="flex items-center justify-between gap-2">
84+
<CardTitle className="line-clamp-1 text-base">
85+
{course.title}
86+
</CardTitle>
87+
<Badge variant="outline" className="text-xs">
88+
{difficultyLabel}
89+
</Badge>
90+
</div>
91+
{date && (
92+
<p className="text-[11px] text-muted-foreground">
93+
Created on{" "}
94+
{date.toLocaleDateString(undefined, {
95+
month: "short",
96+
day: "numeric",
97+
year: "numeric",
98+
})}
99+
</p>
100+
)}
101+
</CardHeader>
102+
<CardContent>
103+
<CardDescription className="line-clamp-2 text-sm">
104+
{course.description || "No description provided yet."}
105+
</CardDescription>
106+
</CardContent>
107+
</Card>
108+
</Link>
109+
);
110+
};
111+
112+
return (
113+
<div className="space-y-6">
114+
{/* Header */}
115+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
116+
<div>
117+
<h1 className="text-2xl font-semibold tracking-tight">
118+
Explore Courses
119+
</h1>
120+
<p className="text-sm text-muted-foreground">
121+
Browse all available courses or jump back into what you&apos;re
122+
working on.
123+
</p>
124+
</div>
125+
126+
{/* Search */}
127+
<div className="w-full sm:w-72">
128+
<Input
129+
value={search}
130+
onChange={(e) => {
131+
setSearch(e.target.value);
132+
// reset pagination when search changes
133+
setVisibleCount(PAGE_SIZE);
134+
}}
135+
placeholder="Search courses…"
136+
/>
137+
</div>
138+
</div>
139+
140+
{/* Tabs */}
141+
<Tabs defaultValue="popular" className="w-full">
142+
<TabsList className="grid w-full grid-cols-3 sm:w-auto">
143+
<TabsTrigger value="popular">Popular</TabsTrigger>
144+
<TabsTrigger value="in-progress">In progress</TabsTrigger>
145+
<TabsTrigger value="all">All courses</TabsTrigger>
146+
</TabsList>
147+
148+
{/* Popular */}
149+
<TabsContent value="popular" className="mt-4 space-y-4">
150+
{popularCourses.length === 0 ? (
151+
<p className="text-sm text-muted-foreground">
152+
No courses to show here yet. Once courses are added and gets
153+
activity, they&apos;ll show up under Popular.
154+
</p>
155+
) : (
156+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
157+
{popularCourses.map(renderCourseCard)}
158+
</div>
159+
)}
160+
</TabsContent>
161+
162+
{/* In progress */}
163+
<TabsContent value="in-progress" className="mt-4 space-y-4">
164+
{inProgressCourses.length === 0 ? (
165+
<p className="text-sm text-muted-foreground">
166+
You don&apos;t have any courses in progress yet. Start any course
167+
to see it here.
168+
</p>
169+
) : (
170+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
171+
{inProgressCourses.map(renderCourseCard)}
172+
</div>
173+
)}
174+
</TabsContent>
175+
176+
{/* All courses with pagination */}
177+
<TabsContent value="all" className="mt-4 space-y-4">
178+
{visibleCourses.length === 0 ? (
179+
<p className="text-sm text-muted-foreground">
180+
No courses match your search.
181+
</p>
182+
) : (
183+
<>
184+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
185+
{visibleCourses.map(renderCourseCard)}
186+
</div>
187+
188+
{hasMore && (
189+
<div className="flex justify-center pt-2">
190+
<Button
191+
variant="outline"
192+
onClick={() =>
193+
setVisibleCount((prev) => prev + PAGE_SIZE)
194+
}
195+
>
196+
Load more
197+
</Button>
198+
</div>
199+
)}
200+
</>
201+
)}
202+
</TabsContent>
203+
</Tabs>
204+
</div>
205+
);
206+
}

src/app/explore/page.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// src/app/explore/page.tsx
2+
import { headers } from "next/headers";
3+
import { auth } from "@/lib/auth";
4+
import { db } from "@/db";
5+
import { courses } from "@/db/schema";
6+
import { desc } from "drizzle-orm";
7+
import { ExploreCoursesClient } from "./ExploreCoursesClient";
8+
9+
export default async function ExplorePage() {
10+
const session = await auth.api.getSession({
11+
headers: await headers(),
12+
});
13+
14+
const inProgressCourseIds: Array<number | string> = [];
15+
16+
const allCourses = await db
17+
.select()
18+
.from(courses)
19+
.orderBy(desc(courses.createdAt));
20+
21+
return (
22+
<div className="max-w-5xl mx-auto py-10 px-4 sm:px-6 lg:px-8">
23+
<ExploreCoursesClient
24+
courses={allCourses.map((c) => ({
25+
id: c.id,
26+
title: c.title,
27+
description: c.description ?? "",
28+
difficulty: c.difficulty,
29+
createdAt: Number(c.createdAt || 0),
30+
}))}
31+
inProgressCourseIds={inProgressCourseIds}
32+
/>
33+
</div>
34+
);
35+
}

0 commit comments

Comments
 (0)