import { notFound, redirect } from "next/navigation"; import { auth } from "@/auth"; 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, }: { params: Promise<{ projectId: string }>; }) { const session = await auth(); if (!session?.user) redirect("/login"); const { projectId } = await params; const project = await prisma.project.findFirst({ where: { id: projectId, ...projectAccessFilter(session.user.id) }, 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". if (!project) notFound(); const [board, aiSettings] = await Promise.all([ getBoard({ projectId: project.id }), getAiSettingsView(), ]); 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 && (