layout.tsx55 lines · main
1import { cookies } from 'next/headers';
2import Link from 'next/link';
3import { notFound } from 'next/navigation';
4
5import { ApiError, apiJson } from '../../../../../lib/api';
6import { ProjectTabs } from './project-tabs';
7
8interface Project {
9 id: string;
10 slug: string;
11 name: string;
12 region: string;
13 tier: 'free' | 'pro' | 'team';
14}
15
16export const dynamic = 'force-dynamic';
17
18export default async function ProjectLayout({
19 params,
20 children,
21}: {
22 params: Promise<{ id: string }>;
23 children: React.ReactNode;
24}) {
25 const { id } = await params;
26 const developerMode = (await cookies()).get('briven_dev')?.value === '1';
27
28 let project: Project;
29 try {
30 const data = await apiJson<{ project: Project }>(`/v1/projects/${id}`);
31 project = data.project;
32 } catch (err) {
33 if (err instanceof ApiError && (err.status === 404 || err.status === 403)) notFound();
34 throw err;
35 }
36
37 return (
38 <div className="flex flex-col gap-6">
39 <header>
40 <Link
41 href="/dashboard/projects"
42 className="font-mono text-xs text-[var(--color-text-subtle)] hover:text-[var(--color-text-muted)]"
43 >
44 ← projects
45 </Link>
46 <h1 className="mt-2 font-mono text-xl tracking-tight">{project.name}</h1>
47 <p className="mt-1 font-mono text-sm text-[var(--color-text-muted)]">
48 {project.slug} · {project.region} · {project.tier}
49 </p>
50 </header>
51 <ProjectTabs projectId={project.id} developerMode={developerMode} />
52 <section>{children}</section>
53 </div>
54 );
55}