layout.tsx115 lines · main
1import Image from 'next/image';
2import Link from 'next/link';
3
4import { DashboardMobileNav } from './dashboard-mobile-nav';
5import { DashboardSidebar } from './dashboard-sidebar';
6import { SignOutButton } from './sign-out-button';
7import { LiveRefresh } from '../../components/live-refresh';
8import { apiJson } from '../../lib/api';
9import { requireUser } from '../../lib/session';
10
11interface BuildInfo {
12 buildSha: string;
13 buildAt: string;
14}
15
16async function fetchBuildInfo(): Promise<BuildInfo | null> {
17 // /info is unauthenticated + cheap. If it ever fails (dev mode,
18 // old deploy), suppress — the footer just doesn't render the sha.
19 try {
20 return await apiJson<BuildInfo>('/info');
21 } catch {
22 return null;
23 }
24}
25
26export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
27 const [user, info] = await Promise.all([requireUser(), fetchBuildInfo()]);
28
29 return (
30 <div className="flex h-dvh flex-col bg-[var(--color-bg)] text-[var(--color-text)]">
31 <LiveRefresh />
32 <header className="shrink-0 border-b border-[var(--color-border-subtle)]">
33 <div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4">
34 <Link href="/dashboard" className="flex items-center gap-3" aria-label="briven dashboard">
35 <Image src="/icon.svg" alt="" width={24} height={24} priority />
36 <span className="font-mono text-sm">briven</span>
37 <span className="font-mono text-xs text-[var(--color-text-subtle)]">· tech</span>
38 <span
39 aria-label="beta v1"
40 title="briven is in public beta — production-ready, on track for GA later this year"
41 className="ml-1 inline-flex items-center rounded-[var(--radius-full)] border border-[var(--color-border-primary)] bg-[var(--color-primary-subtle)] px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-wider text-[var(--color-primary)]"
42 >
43 beta v1
44 </span>
45 </Link>
46
47 <div className="flex items-center gap-4">
48 <span
49 className="font-mono text-xs text-[var(--color-text-muted)]"
50 title={user.email}
51 aria-label="signed in user"
52 >
53 {/* Per CLAUDE.md §5.1 avoid showing full email; prefer name fallback. */}
54 {user.name ?? 'signed in'}
55 </span>
56 <SignOutButton />
57 </div>
58 </div>
59 </header>
60
61 {/* Mobile-only nav row. Hidden md+ where the sidebar takes over. */}
62 <DashboardMobileNav />
63
64 {/* Body area fills the remaining viewport height. Sidebar spans the
65 full body height; main scrolls independently so the sidebar's
66 bottom-anchored toggle stays put. Padding tightens on mobile so
67 the main content gets the full viewport width. */}
68 <div className="mx-auto flex min-h-0 w-full max-w-6xl flex-1 gap-4 px-4 py-4 md:gap-8 md:px-6 md:py-8">
69 <DashboardSidebar
70 isAdmin={user.isAdmin}
71 user={{
72 name: user.name,
73 email: user.email,
74 image: user.image,
75 legalName: user.legalName,
76 }}
77 />
78 <main className="min-w-0 flex-1 overflow-y-auto">{children}</main>
79 </div>
80
81 <footer className="shrink-0 border-t border-[var(--color-border-subtle)]">
82 <div className="mx-auto flex w-full max-w-6xl items-end justify-between px-6 py-3 font-mono text-[10px] text-[var(--color-text-subtle)]">
83 <div className="flex flex-col gap-0.5">
84 <span className="text-[var(--color-text-muted)]">briven · briven.tech</span>
85 <span>
86 made with <span className="text-[#e8344a]">♥</span> in Flanders by flndrn
87 </span>
88 <span>100% self-funded, sustainable &amp; independent</span>
89 <span>flndrn Limited, Limassol, Cyprus</span>
90 </div>
91 <div className="flex items-center gap-3">
92 <Link href="https://docs.briven.tech" className="hover:text-[var(--color-text-muted)]">
93 docs
94 </Link>
95 <Link
96 href="https://docs.briven.tech/support"
97 className="hover:text-[var(--color-text-muted)]"
98 >
99 support
100 </Link>
101 <Link
102 href="https://docs.briven.tech/status"
103 className="hover:text-[var(--color-text-muted)]"
104 >
105 status
106 </Link>
107 {info ? (
108 <span title={`built ${info.buildAt}`}>build {info.buildSha.slice(0, 7)}</span>
109 ) : null}
110 </div>
111 </div>
112 </footer>
113 </div>
114 );
115}