Skip to content

Commit 570e57a

Browse files
authored
Merge pull request #68 from acmutsa/ExplorePage
Explore Page Created
2 parents ee48121 + ea64601 commit 570e57a

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed

src/app/explore/page.tsx

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

0 commit comments

Comments
 (0)