diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx index d080c78..4723097 100644 --- a/app/(app)/layout.tsx +++ b/app/(app)/layout.tsx @@ -2,6 +2,8 @@ import { redirect } from "next/navigation"; import { auth } from "@/auth"; import { prisma } from "@/lib/db"; +import type { ThemeName } from "@/lib/themes"; +import type { ProjectDTO } from "@/types/project"; import { SideNavProvider } from "@/components/nav/side-nav-provider"; import { SideNav } from "@/components/nav/side-nav"; import { ProjectsProvider } from "@/components/projects/projects-context"; @@ -18,11 +20,19 @@ export default async function AppLayout({ children }: { children: React.ReactNod // Just the list for the sidebar/`/projects` page -- each project's own // board (categories/groups/todos) is fetched separately by its own page. - const projects = await prisma.project.findMany({ - where: { ownerId: session.user.id }, - orderBy: { createdAt: "asc" }, - select: { id: true, title: true }, - }); + // `theme` is stored as free text; the set of valid names is enforced + // client-side (ProjectThemeSchema) so a plain assertion is safe here. + const projects: ProjectDTO[] = ( + await prisma.project.findMany({ + where: { ownerId: session.user.id }, + orderBy: { createdAt: "asc" }, + select: { id: true, title: true, theme: true }, + }) + ).map((p) => ({ + id: p.id, + title: p.title, + theme: p.theme as ThemeName | null, + })); return ( diff --git a/app/(app)/projects/[projectId]/page.tsx b/app/(app)/projects/[projectId]/page.tsx index 35e784d..489471f 100644 --- a/app/(app)/projects/[projectId]/page.tsx +++ b/app/(app)/projects/[projectId]/page.tsx @@ -5,7 +5,9 @@ import { prisma } from "@/lib/db"; import { projectAccessFilter } from "@/lib/access"; import { getBoard } from "@/lib/board"; import { getAiSettingsView } from "@/lib/ai-settings"; +import { themeSwitchScript, type ThemeName } from "@/lib/themes"; import { KanbanBoard } from "@/components/board/kanban-board"; +import { ProjectThemeScope } from "@/components/theme/project-theme-scope"; export default async function ProjectPage({ params, @@ -18,7 +20,7 @@ export default async function ProjectPage({ const project = await prisma.project.findFirst({ where: { id: projectId, ...projectAccessFilter(session.user.id) }, - select: { id: true, title: true }, + select: { id: true, title: true, theme: true }, }); // Same response whether the project doesn't exist or just isn't this // user's -- no need to distinguish "not found" from "not yours". @@ -30,12 +32,28 @@ export default async function ProjectPage({ ]); const aiConfigured = !!(aiSettings.apiUrl && aiSettings.model); + // A project's assigned theme (null = "Default Theme", i.e. the user's + // global theme-menu choice) applies while this page is open and lifts on + // navigation -- ProjectThemeScope owns the client side. The inline script + // mirrors it on before first paint on a hard load, so the first + // frame is already themed (and leaves the data-project-theme marker the + // ThemeProvider reads at hydration). `theme` is free text in the DB; the + // set of valid names is enforced by ProjectThemeSchema on write, so the + // assertion is safe. + const scopedTheme: ThemeName | null = project.theme as ThemeName | null; + return ( - + <> + {scopedTheme && ( +