From f39d22a1b065f06c88c0b44bbdb06b4a48a6a186 Mon Sep 17 00:00:00 2001 From: Derick Gomez Date: Sun, 30 Nov 2025 23:24:49 -0600 Subject: [PATCH 1/5] Explore Page Created --- src/app/explore/ExploreCoursesClient.tsx | 206 +++++++++++++++++++++++ src/app/explore/page.tsx | 35 ++++ 2 files changed, 241 insertions(+) create mode 100644 src/app/explore/ExploreCoursesClient.tsx create mode 100644 src/app/explore/page.tsx diff --git a/src/app/explore/ExploreCoursesClient.tsx b/src/app/explore/ExploreCoursesClient.tsx new file mode 100644 index 0000000..fa8fdeb --- /dev/null +++ b/src/app/explore/ExploreCoursesClient.tsx @@ -0,0 +1,206 @@ +// src/app/courses/ExploreCoursesClient.tsx +"use client"; + +import { useMemo, useState } from "react"; +import Link from "next/link"; + +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent, +} from "@/components/ui/tabs"; + +type CourseItem = { + id: number | string; + title: string; + description: string; + difficulty: string | null; + createdAt: number; +}; + +type ExploreCoursesClientProps = { + courses: CourseItem[]; + inProgressCourseIds: Array; +}; + +const PAGE_SIZE = 10; + +export function ExploreCoursesClient({ + courses, + inProgressCourseIds, +}: ExploreCoursesClientProps) { + const [search, setSearch] = useState(""); + const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); + + const normalizedSearch = search.trim().toLowerCase(); + + const filteredCourses = useMemo(() => { + if (!normalizedSearch) return courses; + return courses.filter((course) => { + const haystack = `${course.title} ${course.description}`.toLowerCase(); + return haystack.includes(normalizedSearch); + }); + }, [courses, normalizedSearch]); + + const popularCourses = useMemo(() => { + return filteredCourses.slice(0, 5); + }, [filteredCourses]); + + const inProgressCourses = useMemo(() => { + if (!inProgressCourseIds.length) return []; + const set = new Set(inProgressCourseIds.map(String)); + return filteredCourses.filter((c) => set.has(String(c.id))); + }, [filteredCourses, inProgressCourseIds]); + + const visibleCourses = filteredCourses.slice(0, visibleCount); + const hasMore = filteredCourses.length > visibleCourses.length; + + const renderCourseCard = (course: CourseItem) => { + const difficultyLabel = + course.difficulty?.charAt(0).toUpperCase() + + course.difficulty?.slice(1) || "Beginner"; + + const date = + course.createdAt && !Number.isNaN(course.createdAt) + ? new Date(course.createdAt * 1000) + : null; + + return ( + + + +
+ + {course.title} + + + {difficultyLabel} + +
+ {date && ( +

+ Created on{" "} + {date.toLocaleDateString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + })} +

+ )} +
+ + + {course.description || "No description provided yet."} + + +
+ + ); + }; + + return ( +
+ {/* Header */} +
+
+

+ Explore Courses +

+

+ Browse all available courses or jump back into what you're + working on. +

+
+ + {/* Search */} +
+ { + setSearch(e.target.value); + // reset pagination when search changes + setVisibleCount(PAGE_SIZE); + }} + placeholder="Search courses…" + /> +
+
+ + {/* Tabs */} + + + Popular + In progress + All courses + + + {/* Popular */} + + {popularCourses.length === 0 ? ( +

+ No courses to show here yet. Once courses are added and gets + activity, they'll show up under Popular. +

+ ) : ( +
+ {popularCourses.map(renderCourseCard)} +
+ )} +
+ + {/* In progress */} + + {inProgressCourses.length === 0 ? ( +

+ You don't have any courses in progress yet. Start any course + to see it here. +

+ ) : ( +
+ {inProgressCourses.map(renderCourseCard)} +
+ )} +
+ + {/* All courses with pagination */} + + {visibleCourses.length === 0 ? ( +

+ No courses match your search. +

+ ) : ( + <> +
+ {visibleCourses.map(renderCourseCard)} +
+ + {hasMore && ( +
+ +
+ )} + + )} +
+
+
+ ); +} diff --git a/src/app/explore/page.tsx b/src/app/explore/page.tsx new file mode 100644 index 0000000..ddc2638 --- /dev/null +++ b/src/app/explore/page.tsx @@ -0,0 +1,35 @@ +// src/app/explore/page.tsx +import { headers } from "next/headers"; +import { auth } from "@/lib/auth"; +import { db } from "@/db"; +import { courses } from "@/db/schema"; +import { desc } from "drizzle-orm"; +import { ExploreCoursesClient } from "./ExploreCoursesClient"; + +export default async function ExplorePage() { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + const inProgressCourseIds: Array = []; + + const allCourses = await db + .select() + .from(courses) + .orderBy(desc(courses.createdAt)); + + return ( +
+ ({ + id: c.id, + title: c.title, + description: c.description ?? "", + difficulty: c.difficulty, + createdAt: Number(c.createdAt || 0), + }))} + inProgressCourseIds={inProgressCourseIds} + /> +
+ ); +} From e4b6171e76d4d65b9d9e231ee645b897b7ac9320 Mon Sep 17 00:00:00 2001 From: Derick Gomez Date: Sun, 30 Nov 2025 23:28:39 -0600 Subject: [PATCH 2/5] Remove accidentally included profile files --- src/actions/update-profile.ts | 42 ---------- src/app/profile/ProfileForm.tsx | 137 -------------------------------- src/app/profile/page.tsx | 55 ------------- 3 files changed, 234 deletions(-) delete mode 100644 src/actions/update-profile.ts delete mode 100644 src/app/profile/ProfileForm.tsx delete mode 100644 src/app/profile/page.tsx diff --git a/src/actions/update-profile.ts b/src/actions/update-profile.ts deleted file mode 100644 index 197467f..0000000 --- a/src/actions/update-profile.ts +++ /dev/null @@ -1,42 +0,0 @@ -// src/actions/update-profile.ts -"use server"; - -import { headers } from "next/headers"; -import { revalidatePath } from "next/cache"; - -import { auth } from "@/lib/auth"; -import { db } from "@/db"; -import { users } from "@/db/schema"; -import { eq } from "drizzle-orm"; - -export async function updateProfile(formData: FormData) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session?.user) { - throw new Error("Not authenticated"); - } - - const userId = session.user.id as any; - - const name = formData.get("name"); - const image = formData.get("image"); - - if (typeof name !== "string" || name.trim().length < 2) { - throw new Error("Name must be at least 2 characters"); - } - - await db - .update(users) - .set({ - name: name.trim(), - image: - typeof image === "string" && image.trim().length > 0 - ? image.trim() - : null, - }) - .where(eq(users.id, userId)); - - revalidatePath("/profile"); -} diff --git a/src/app/profile/ProfileForm.tsx b/src/app/profile/ProfileForm.tsx deleted file mode 100644 index 8b1081d..0000000 --- a/src/app/profile/ProfileForm.tsx +++ /dev/null @@ -1,137 +0,0 @@ -// src/app/profile/ProfileForm.tsx - -"use client"; - -import { useTransition, FormEvent } from "react"; -import { Loader2 } from "lucide-react"; - -import { - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, - CardFooter, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Avatar, - AvatarImage, - AvatarFallback, -} from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { toast } from "sonner"; -import { updateProfile } from "@/actions/update-profile"; - -type ProfileFormProps = { - name: string; - email: string; - image: string | null; - initials: string; -}; - -export function ProfileForm({ name, email, image, initials }: ProfileFormProps) { - const [isPending, startTransition] = useTransition(); - - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - - startTransition(async () => { - try { - await updateProfile(formData); - toast.success("Profile updated", { - description: "Your changes have been saved.", - }); - } catch (err) { - console.error(err); - toast.error("Update failed", { - description: "Something went wrong while saving your profile.", - }); - } - }); - }; - - return ( - - -
- - {image ? ( - - ) : ( - <> - - {initials} - - )} - - -
- Your Profile - - View and update your account information. - -
-
-
- -
- - {/* Name (editable) */} -
- - -
- - {/* Email (read-only) */} -
- - -

- Email is managed by authentication and cannot be changed here. -

-
- - {/* Avatar URL (editable) */} -
- - -

- Paste a direct image URL. Later you can replace this with an - upload flow using your S3/blob system. -

-
-
- - - - -
-
- ); -} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx deleted file mode 100644 index a6e1a89..0000000 --- a/src/app/profile/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -// src/app/profile/page.tsx -import { redirect } from "next/navigation"; -import { headers } from "next/headers"; - -import { auth } from "@/lib/auth"; -import { db } from "@/db"; -import { users } from "@/db/schema"; -import { eq } from "drizzle-orm"; -import { ProfileForm } from "./ProfileForm"; - -export default async function ProfilePage() { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session?.user) { - redirect("/sign-in"); - } - - const userId = session.user.id as any; - - const [user] = await db - .select() - .from(users) - .where(eq(users.id, userId)) - .limit(1); - - if (!user) { - redirect("/"); - } - - const displayName = user.name ?? ""; - const email = user.email ?? ""; - const image = user.image ?? null; - - const initials = - displayName && displayName.trim().length > 0 - ? displayName - .split(" ") - .map((part: string) => part[0]) - .join("") - .toUpperCase() - : "U"; - - return ( -
- -
- ); -} From c6350fb7d6d24171a57ad7ddce2b8aeda5ba8f88 Mon Sep 17 00:00:00 2001 From: adanrsantos Date: Mon, 1 Dec 2025 12:40:56 -0600 Subject: [PATCH 3/5] added back update-profile.ts --- src/actions/update-profile.ts | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/actions/update-profile.ts diff --git a/src/actions/update-profile.ts b/src/actions/update-profile.ts new file mode 100644 index 0000000..9f4cd5c --- /dev/null +++ b/src/actions/update-profile.ts @@ -0,0 +1,42 @@ +// src/actions/update-profile.ts +"use server"; + +import { headers } from "next/headers"; +import { revalidatePath } from "next/cache"; + +import { auth } from "@/lib/auth"; +import { db } from "@/db"; +import { users } from "@/db/schema"; +import { eq } from "drizzle-orm"; + +export async function updateProfile(formData: FormData) { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session?.user) { + throw new Error("Not authenticated"); + } + + const userId = session.user.id as any; + + const name = formData.get("name"); + const image = formData.get("image"); + + if (typeof name !== "string" || name.trim().length < 2) { + throw new Error("Name must be at least 2 characters"); + } + + await db + .update(users) + .set({ + name: name.trim(), + image: + typeof image === "string" && image.trim().length > 0 + ? image.trim() + : null, + }) + .where(eq(users.id, userId)); + + revalidatePath("/profile"); +} \ No newline at end of file From 7e4d7aae0ccb28c3cc7f38b1245fe61a5a587a43 Mon Sep 17 00:00:00 2001 From: adanrsantos Date: Mon, 1 Dec 2025 12:41:26 -0600 Subject: [PATCH 4/5] fix --- src/actions/update-profile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/update-profile.ts b/src/actions/update-profile.ts index 9f4cd5c..197467f 100644 --- a/src/actions/update-profile.ts +++ b/src/actions/update-profile.ts @@ -39,4 +39,4 @@ export async function updateProfile(formData: FormData) { .where(eq(users.id, userId)); revalidatePath("/profile"); -} \ No newline at end of file +} From ea646011a78510e9024fa011768f93cb944e4653 Mon Sep 17 00:00:00 2001 From: adanrsantos Date: Mon, 1 Dec 2025 12:46:27 -0600 Subject: [PATCH 5/5] nice work --- src/app/explore/page.tsx | 5 ++--- src/{app/explore => components}/ExploreCoursesClient.tsx | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) rename src/{app/explore => components}/ExploreCoursesClient.tsx (99%) diff --git a/src/app/explore/page.tsx b/src/app/explore/page.tsx index ddc2638..440e6a2 100644 --- a/src/app/explore/page.tsx +++ b/src/app/explore/page.tsx @@ -1,10 +1,9 @@ -// src/app/explore/page.tsx import { headers } from "next/headers"; import { auth } from "@/lib/auth"; import { db } from "@/db"; import { courses } from "@/db/schema"; import { desc } from "drizzle-orm"; -import { ExploreCoursesClient } from "./ExploreCoursesClient"; +import { ExploreCoursesClient } from "@/components/ExploreCoursesClient"; export default async function ExplorePage() { const session = await auth.api.getSession({ @@ -19,7 +18,7 @@ export default async function ExplorePage() { .orderBy(desc(courses.createdAt)); return ( -
+
({ id: c.id, diff --git a/src/app/explore/ExploreCoursesClient.tsx b/src/components/ExploreCoursesClient.tsx similarity index 99% rename from src/app/explore/ExploreCoursesClient.tsx rename to src/components/ExploreCoursesClient.tsx index fa8fdeb..745d4f2 100644 --- a/src/app/explore/ExploreCoursesClient.tsx +++ b/src/components/ExploreCoursesClient.tsx @@ -1,4 +1,3 @@ -// src/app/courses/ExploreCoursesClient.tsx "use client"; import { useMemo, useState } from "react";