layout.tsx115 lines · main
| 1 | import Image from 'next/image'; |
| 2 | import Link from 'next/link'; |
| 3 | |
| 4 | import { DashboardMobileNav } from './dashboard-mobile-nav'; |
| 5 | import { DashboardSidebar } from './dashboard-sidebar'; |
| 6 | import { SignOutButton } from './sign-out-button'; |
| 7 | import { LiveRefresh } from '../../components/live-refresh'; |
| 8 | import { apiJson } from '../../lib/api'; |
| 9 | import { requireUser } from '../../lib/session'; |
| 10 | |
| 11 | interface BuildInfo { |
| 12 | buildSha: string; |
| 13 | buildAt: string; |
| 14 | } |
| 15 | |
| 16 | async 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 | |
| 26 | export 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 & 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 | } |