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 { MobileTopBar } from "@/components/nav/mobile-top-bar"; import { ProjectsProvider } from "@/components/projects/projects-context"; import { ScheduledPanelProvider } from "@/components/scheduled/scheduled-panel-provider"; import { ScheduledPanel } from "@/components/scheduled/scheduled-panel"; import { BoardViewProvider } from "@/components/board/board-view-provider"; import { HoldOnCompleteProvider } from "@/components/hold-on-complete"; export default async function AppLayout({ children }: { children: React.ReactNode }) { const session = await auth(); // Defense in depth: proxy.ts already redirects unauthenticated requests, // but every protected data boundary should check for itself too. if (!session?.user) redirect("/login"); // Just the list for the sidebar/`/projects` page -- each project's own // board (categories/groups/todos) is fetched separately by its own page. // `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, profile] = await Promise.all([ prisma.project.findMany({ where: { ownerId: session.user.id }, orderBy: { createdAt: "asc" }, select: { id: true, title: true, theme: true }, }), // The user's profile for the sidebar's bottom user block (name + photo, // both optional); the /profile page fetches the same row itself. prisma.user.findUnique({ where: { id: session.user.id }, select: { firstName: true, lastName: true, avatar: true }, }), ]); const projectList: ProjectDTO[] = projects.map((p) => ({ id: p.id, title: p.title, theme: p.theme as ThemeName | null, })); const userName = [profile?.firstName, profile?.lastName] .filter((part): part is string => Boolean(part && part.trim())) .map((part) => part.trim()) .join(" ") || null; return ( {/* Responsive shell: - md and up: the original three-column desktop layout, with the same min-width + horizontal-scroll fallback as before. - below md (phones): a true stacked app layout -- top bar, full-height board, and the Scheduled dock (rendered inside ScheduledPanel) take the full column with no horizontal scrolling; h-dvh tracks the mobile browser's dynamic toolbar instead of the stale 100vh. */}
{children}
); }