From 6729f128a7670a5b1b5368d483a572a53b04d90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20P=C3=A9rez-Aradros=20Herce?= Date: Wed, 27 May 2026 10:29:13 +0200 Subject: [PATCH 1/2] Unify dashboard & lessons page These pages were too similar, handle them from a single place, allowing both annonymous & logged in users to visit it --- app/(app)/dashboard/page.tsx | 120 +++++++++++++++++------------ app/lessons/[slug]/page.tsx | 2 +- app/lessons/page.tsx | 145 ----------------------------------- app/page.tsx | 2 +- components/SignOutButton.tsx | 31 -------- proxy.ts | 28 ------- 6 files changed, 73 insertions(+), 255 deletions(-) delete mode 100644 app/lessons/page.tsx delete mode 100644 components/SignOutButton.tsx delete mode 100644 proxy.ts diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx index 37b5d8f..76441f4 100644 --- a/app/(app)/dashboard/page.tsx +++ b/app/(app)/dashboard/page.tsx @@ -1,10 +1,10 @@ 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"; +import { SignInButton } from "@/app/sign-in-button"; type Bucket = "continue" | "completed" | "available"; @@ -24,15 +24,18 @@ function classify(passed: number, total: number): Bucket { } export default async function DashboardPage() { - const session = await auth.api.getSession({ headers: await headers() }); - if (!session) redirect("/"); + const session = await auth.api + .getSession({ headers: await headers() }) + .catch(() => null); 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 progress = session + ? await getProgressCounts( + session.user.id, + allLessons.map((l) => l.meta.slug), + ) + : new Map(); const stateBySlug = new Map(); for (const lesson of allLessons) { @@ -52,37 +55,47 @@ export default async function DashboardPage() { 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. - )} -

+
+
+

+ {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 && ( +

+ {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. + )} +

+ )}
@@ -106,17 +119,30 @@ export default async function DashboardPage() {
  • - + {String(lesson.meta.order).padStart(2, "0")} - - {lesson.meta.title} - - {total > 0 && ( +
    + + {lesson.meta.title} + +
    + {lesson.meta.estimatedMinutes} min + {lesson.meta.tags.length > 0 && ( + <> + · + + {lesson.meta.tags.join(" · ")} + + + )} +
    +
    + {session && total > 0 && ( {bucket === "completed" && ( @@ -134,10 +160,6 @@ export default async function DashboardPage() { ))}
  • - -
    - -
    ); } diff --git a/app/lessons/[slug]/page.tsx b/app/lessons/[slug]/page.tsx index 92df4a6..907d0e8 100644 --- a/app/lessons/[slug]/page.tsx +++ b/app/lessons/[slug]/page.tsx @@ -54,7 +54,7 @@ export default async function LessonPage({
    - + ← All lessons
    diff --git a/app/lessons/page.tsx b/app/lessons/page.tsx deleted file mode 100644 index 815712e..0000000 --- a/app/lessons/page.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import Link from "next/link"; -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"; - -export const metadata = { title: "Lessons — Learn Postgres" }; - -const difficultyBadge: Record = { - beginner: - "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", -}; - -export default async function LessonsCatalogPage() { - const [modules, session] = await Promise.all([ - getModules(), - auth.api.getSession({ headers: await headers() }).catch(() => null), - ]); - - const allSlugs = modules.flatMap((m) => m.lessons.map((l) => l.meta.slug)); - const progress = session - ? await getProgressCounts(session.user.id, allSlugs) - : new Map(); - - 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 && ( -
    - - {session.user.name ?? session.user.email} - - -
    - )} -
    - - {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}

    - )} - -
      - {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 ( -
    • -
      -

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

      - {lesson.meta.summary && ( -

      - {lesson.meta.summary} -

      - )} -
      -
      - {lesson.meta.estimatedMinutes} min - {lesson.meta.tags.length > 0 && ( - <> - · - - {lesson.meta.tags.join(" · ")} - - - )} - {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 - - )} -
      -
    • - ); - })} -
    -
    - ))} -
    - )} -
    - ); -} diff --git a/app/page.tsx b/app/page.tsx index e8935dc..cec295c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -16,7 +16,7 @@ export default function Home() {
    Browse lessons 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*"], -}; From f86154f2fc29a78f9ffb87516bfd96d7075587f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20P=C3=A9rez-Aradros=20Herce?= Date: Wed, 27 May 2026 10:31:22 +0200 Subject: [PATCH 2/2] rename --- app/lessons/[slug]/page.tsx | 2 +- app/{(app)/dashboard => lessons}/page.tsx | 0 app/{(app)/dashboard => lessons}/sign-out-button.tsx | 0 app/page.tsx | 2 +- app/sign-in-button.tsx | 2 +- 5 files changed, 3 insertions(+), 3 deletions(-) rename app/{(app)/dashboard => lessons}/page.tsx (100%) rename app/{(app)/dashboard => lessons}/sign-out-button.tsx (100%) diff --git a/app/lessons/[slug]/page.tsx b/app/lessons/[slug]/page.tsx index 907d0e8..92df4a6 100644 --- a/app/lessons/[slug]/page.tsx +++ b/app/lessons/[slug]/page.tsx @@ -54,7 +54,7 @@ export default async function LessonPage({
    - + ← All lessons
    diff --git a/app/(app)/dashboard/page.tsx b/app/lessons/page.tsx similarity index 100% rename from app/(app)/dashboard/page.tsx rename to app/lessons/page.tsx 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/page.tsx b/app/page.tsx index cec295c..e8935dc 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -16,7 +16,7 @@ export default function Home() {
    Browse lessons 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,