diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx deleted file mode 100644 index 37b5d8f..0000000 --- a/app/(app)/dashboard/page.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import Link from "next/link"; -import { headers } from "next/headers"; -import { redirect } from "next/navigation"; -import { auth } from "@/lib/auth"; -import { getModules } from "@/lib/lessons"; -import { getProgressCounts } from "@/lib/lesson-progress"; -import { SignOutButton } from "./sign-out-button"; - -type Bucket = "continue" | "completed" | "available"; - -type Progress = { passed: number; total: number; bucket: Bucket }; - -const PILL_TONE: Record = { - continue: "bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300", - completed: - "bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-300", - available: "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300", -}; - -function classify(passed: number, total: number): Bucket { - if (total > 0 && passed === total) return "completed"; - if (passed > 0 && passed < total) return "continue"; - return "available"; -} - -export default async function DashboardPage() { - const session = await auth.api.getSession({ headers: await headers() }); - if (!session) redirect("/"); - - const modules = await getModules(); - const allLessons = modules.flatMap((m) => m.lessons); - const progress = await getProgressCounts( - session.user.id, - allLessons.map((l) => l.meta.slug), - ); - - const stateBySlug = new Map(); - for (const lesson of allLessons) { - const total = lesson.meta.checks.length; - const passed = progress.get(lesson.meta.slug) ?? 0; - stateBySlug.set(lesson.meta.slug, { - passed, - total, - bucket: classify(passed, total), - }); - } - - const states = [...stateBySlug.values()]; - const completedCount = states.filter((s) => s.bucket === "completed").length; - const startedCount = states.filter((s) => s.bucket === "continue").length; - - return ( -
-
-

- Welcome, {session.user.name} -

-

- Signed in as {session.user.email}. -

-

- {completedCount > 0 ? ( - <> - You've completed{" "} - {completedCount}{" "} - {completedCount === 1 ? "lesson" : "lessons"} - {startedCount > 0 ? ( - <> - {" "}and have{" "} - {startedCount} in - progress. - - ) : ( - "." - )} - - ) : startedCount > 0 ? ( - <> - You have {startedCount}{" "} - {startedCount === 1 ? "lesson" : "lessons"} in progress. - - ) : ( - <>Pick a lesson below to begin. - )} -

-
- -
- {modules.map(({ module, lessons }) => ( -
-
-

- {module.title} -

- - Module {module.order} - -
- -
    - {lessons.map((lesson) => { - const { passed, total, bucket } = stateBySlug.get( - lesson.meta.slug, - )!; - return ( -
  • - - - {String(lesson.meta.order).padStart(2, "0")} - - - {lesson.meta.title} - - {total > 0 && ( - - {bucket === "completed" && ( - - ✓ - - )} - {passed}/{total} - - )} - -
  • - ); - })} -
-
- ))} -
- -
- -
-
- ); -} diff --git a/app/lessons/page.tsx b/app/lessons/page.tsx index 815712e..76441f4 100644 --- a/app/lessons/page.tsx +++ b/app/lessons/page.tsx @@ -3,143 +3,163 @@ import { headers } from "next/headers"; import { auth } from "@/lib/auth"; import { getModules } from "@/lib/lessons"; import { getProgressCounts } from "@/lib/lesson-progress"; -import { SignOutButton } from "@/components/SignOutButton"; +import { SignOutButton } from "./sign-out-button"; +import { SignInButton } from "@/app/sign-in-button"; -export const metadata = { title: "Lessons — Learn Postgres" }; +type Bucket = "continue" | "completed" | "available"; -const difficultyBadge: Record = { - beginner: +type Progress = { passed: number; total: number; bucket: Bucket }; + +const PILL_TONE: Record = { + continue: "bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300", + completed: "bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-300", - intermediate: - "bg-sky-50 text-sky-700 dark:bg-sky-950/40 dark:text-sky-300", - advanced: - "bg-rose-50 text-rose-700 dark:bg-rose-950/40 dark:text-rose-300", + available: "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300", }; -export default async function LessonsCatalogPage() { - const [modules, session] = await Promise.all([ - getModules(), - auth.api.getSession({ headers: await headers() }).catch(() => null), - ]); +function classify(passed: number, total: number): Bucket { + if (total > 0 && passed === total) return "completed"; + if (passed > 0 && passed < total) return "continue"; + return "available"; +} + +export default async function DashboardPage() { + const session = await auth.api + .getSession({ headers: await headers() }) + .catch(() => null); - const allSlugs = modules.flatMap((m) => m.lessons.map((l) => l.meta.slug)); + const modules = await getModules(); + const allLessons = modules.flatMap((m) => m.lessons); const progress = session - ? await getProgressCounts(session.user.id, allSlugs) + ? await getProgressCounts( + session.user.id, + allLessons.map((l) => l.meta.slug), + ) : new Map(); + const stateBySlug = new Map(); + for (const lesson of allLessons) { + const total = lesson.meta.checks.length; + const passed = progress.get(lesson.meta.slug) ?? 0; + stateBySlug.set(lesson.meta.slug, { + passed, + total, + bucket: classify(passed, total), + }); + } + + const states = [...stateBySlug.values()]; + const completedCount = states.filter((s) => s.bucket === "completed").length; + const startedCount = states.filter((s) => s.bucket === "continue").length; + return (
-
-
-

- Lessons -

-

- {session - ? "Short, hands-on Postgres exercises. Pick one to run in your sandbox." - : "Short, hands-on Postgres exercises. Sign in to run them in your own sandbox."} -

+
+
+
+

+ {session ? `Welcome, ${session.user.name}` : "Lessons"} +

+

+ {session + ? `Signed in as ${session.user.email}.` + : "Short, hands-on Postgres exercises. Sign in to run them in your own sandbox and track your progress."} +

+
+
{session ? : }
{session && ( -
- - {session.user.name ?? session.user.email} - - -
+

+ {completedCount > 0 ? ( + <> + You've completed{" "} + {completedCount}{" "} + {completedCount === 1 ? "lesson" : "lessons"} + {startedCount > 0 ? ( + <> + {" "}and have{" "} + {startedCount} in + progress. + + ) : ( + "." + )} + + ) : startedCount > 0 ? ( + <> + You have{" "} + {startedCount}{" "} + {startedCount === 1 ? "lesson" : "lessons"} in progress. + + ) : ( + <>Pick a lesson below to begin. + )} +

)} -
+ - {modules.length === 0 ? ( -
- No lessons yet. Add the first one under{" "} - /lessons. -
- ) : ( -
- {modules.map(({ module, lessons }) => ( -
-
-

- {module.title} -

- - {module.difficulty} - - - Module {module.order} - -
- {module.summary && ( -

{module.summary}

- )} +
+ {modules.map(({ module, lessons }) => ( +
+
+

+ {module.title} +

+ + Module {module.order} + +
-
    - {lessons.map((lesson) => { - const total = lesson.meta.checks.length; - const passed = progress.get(lesson.meta.slug) ?? 0; - const showProgress = !!session && total > 0; - const complete = showProgress && passed === total; - return ( -
  • + {lessons.map((lesson) => { + const { passed, total, bucket } = stateBySlug.get( + lesson.meta.slug, + )!; + return ( +
  • + -
    -

    - - - {String(lesson.meta.order).padStart(2, "0")}. - {" "} - {lesson.meta.title} - -

    - {lesson.meta.summary && ( -

    - {lesson.meta.summary} -

    - )} + + {String(lesson.meta.order).padStart(2, "0")} + +
    + + {lesson.meta.title} + +
    + {lesson.meta.estimatedMinutes} min + {lesson.meta.tags.length > 0 && ( + <> + · + + {lesson.meta.tags.join(" · ")} + + + )} +
    -
    - {lesson.meta.estimatedMinutes} min - {lesson.meta.tags.length > 0 && ( - <> - · - - {lesson.meta.tags.join(" · ")} + {session && total > 0 && ( + + {bucket === "completed" && ( + + ✓ - - )} - {showProgress && ( - 0 - ? "bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300" - : "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300" - }`} - > - {complete && } - {passed}/{total} checks - - )} -
    -
  • - ); - })} -
-
- ))} -
- )} + )} + {passed}/{total} + + )} + + + ); + })} + +
+ ))} +
); } diff --git a/app/(app)/dashboard/sign-out-button.tsx b/app/lessons/sign-out-button.tsx similarity index 100% rename from app/(app)/dashboard/sign-out-button.tsx rename to app/lessons/sign-out-button.tsx diff --git a/app/sign-in-button.tsx b/app/sign-in-button.tsx index 41dd810..ff6ca50 100644 --- a/app/sign-in-button.tsx +++ b/app/sign-in-button.tsx @@ -13,7 +13,7 @@ type Props = { }; export function SignInButton({ - callbackURL = "/dashboard", + callbackURL = "/lessons", variant = "default", children, preserveScroll = false, diff --git a/components/SignOutButton.tsx b/components/SignOutButton.tsx deleted file mode 100644 index 2a3c303..0000000 --- a/components/SignOutButton.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import { signOut } from "@/lib/auth-client"; - -export function SignOutButton({ className }: { className?: string }) { - const router = useRouter(); - const [loading, setLoading] = useState(false); - - const onClick = async () => { - setLoading(true); - await signOut(); - router.push("/"); - router.refresh(); - }; - - return ( - - ); -} diff --git a/proxy.ts b/proxy.ts deleted file mode 100644 index b1fa88b..0000000 --- a/proxy.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextResponse, type NextRequest } from "next/server"; -import { getSessionCookie } from "better-auth/cookies"; - -// /lessons/[slug] is also protected but enforced at the page level so that -// the public catalog (/lessons) and read-only preview pages stay open. -const PROTECTED_PREFIXES = ["/dashboard"]; - -export function proxy(request: NextRequest) { - const { pathname } = request.nextUrl; - const needsAuth = PROTECTED_PREFIXES.some( - (p) => pathname === p || pathname.startsWith(`${p}/`), - ); - if (!needsAuth) return NextResponse.next(); - - // Optimistic check: cookie presence only. Pages still verify the session. - const cookie = getSessionCookie(request); - if (!cookie) { - const url = request.nextUrl.clone(); - url.pathname = "/"; - url.search = ""; - return NextResponse.redirect(url); - } - return NextResponse.next(); -} - -export const config = { - matcher: ["/dashboard/:path*"], -};