84 lines
3.5 KiB
TypeScript
84 lines
3.5 KiB
TypeScript
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 (
|
|
<SideNavProvider>
|
|
<ScheduledPanelProvider>
|
|
<ProjectsProvider initialProjects={projectList}>
|
|
<BoardViewProvider>
|
|
<HoldOnCompleteProvider>
|
|
{/* 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. */}
|
|
<div className="flex h-dvh flex-col md:h-screen md:min-w-[860px] md:flex-row md:overflow-x-auto">
|
|
<MobileTopBar />
|
|
<SideNav
|
|
userEmail={session.user.email ?? ""}
|
|
userName={userName}
|
|
avatar={profile?.avatar ?? null}
|
|
role={session.user.role}
|
|
/>
|
|
<main className="min-h-0 flex-1 overflow-auto">{children}</main>
|
|
<ScheduledPanel />
|
|
</div>
|
|
</HoldOnCompleteProvider>
|
|
</BoardViewProvider>
|
|
</ProjectsProvider>
|
|
</ScheduledPanelProvider>
|
|
</SideNavProvider>
|
|
);
|
|
}
|