page.tsx115 lines · main
| 1 | import Image from 'next/image'; |
| 2 | |
| 3 | export const metadata = { title: 'down for maintenance · briven' }; |
| 4 | // Always render live state — never a cached splash that shows the wrong |
| 5 | // end time after the window moves. |
| 6 | export const dynamic = 'force-dynamic'; |
| 7 | |
| 8 | /** The public maintenance contract from apps/api. */ |
| 9 | interface MaintenanceStatus { |
| 10 | active: boolean; |
| 11 | scheduled: boolean; |
| 12 | upcoming: boolean; |
| 13 | startsAt: string | null; |
| 14 | endsAt: string | null; |
| 15 | message: string | null; |
| 16 | } |
| 17 | |
| 18 | /** |
| 19 | * Fetch the maintenance window straight from the public api. Self-contained: |
| 20 | * no cookies, no gated data, so it renders even while the rest of the site is |
| 21 | * gated behind the maintenance rewrite. Fails soft to nulls so the page still |
| 22 | * shows the base "down for maintenance" message if the api is unreachable. |
| 23 | */ |
| 24 | async function getMaintenance(): Promise<MaintenanceStatus | null> { |
| 25 | const origin = process.env.NEXT_PUBLIC_BRIVEN_API_ORIGIN ?? ''; |
| 26 | if (!origin) return null; |
| 27 | try { |
| 28 | const res = await fetch(`${origin}/v1/status/maintenance`, { |
| 29 | cache: 'no-store', |
| 30 | headers: { accept: 'application/json' }, |
| 31 | }); |
| 32 | if (!res.ok) return null; |
| 33 | return (await res.json()) as MaintenanceStatus; |
| 34 | } catch { |
| 35 | return null; |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | /** "Wed 3 Jul, 14:30" — a friendly local time the visitor can read. */ |
| 40 | function formatWindowEnd(iso: string): string { |
| 41 | const d = new Date(iso); |
| 42 | if (Number.isNaN(d.getTime())) return ''; |
| 43 | return new Intl.DateTimeFormat('en-GB', { |
| 44 | weekday: 'short', |
| 45 | day: 'numeric', |
| 46 | month: 'short', |
| 47 | hour: '2-digit', |
| 48 | minute: '2-digit', |
| 49 | }).format(d); |
| 50 | } |
| 51 | |
| 52 | export default async function MaintenancePage() { |
| 53 | const status = await getMaintenance(); |
| 54 | const endLabel = status?.endsAt ? formatWindowEnd(status.endsAt) : ''; |
| 55 | const customMessage = status?.message?.trim() || ''; |
| 56 | |
| 57 | return ( |
| 58 | <main className="relative flex min-h-dvh flex-col items-center justify-center bg-[var(--color-bg)] px-6 py-16 text-[var(--color-text)]"> |
| 59 | <div className="flex w-full max-w-lg flex-col items-center gap-8 text-center"> |
| 60 | <div className="flex items-center gap-2" aria-label="briven"> |
| 61 | <Image src="/icon.svg" alt="" width={36} height={36} priority className="opacity-95" /> |
| 62 | <span className="font-mono tracking-tight text-[var(--color-text)] text-[var(--text-small)]"> |
| 63 | briven |
| 64 | </span> |
| 65 | </div> |
| 66 | |
| 67 | <div className="flex flex-col gap-4"> |
| 68 | <span className="inline-flex items-center justify-center gap-2 self-center rounded-[var(--radius-full)] border border-[var(--color-border-primary)] bg-[var(--color-primary-subtle)] px-3 py-1 font-mono uppercase tracking-wider text-[var(--color-primary)] text-[10px]"> |
| 69 | <span className="relative flex h-1.5 w-1.5"> |
| 70 | <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--color-primary)] opacity-60" /> |
| 71 | <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[var(--color-primary)]" /> |
| 72 | </span> |
| 73 | maintenance in progress |
| 74 | </span> |
| 75 | |
| 76 | <h1 className="font-sans font-medium leading-[1.1] tracking-[-0.03em] text-[var(--color-text)] text-[var(--text-display-3)]"> |
| 77 | down for maintenance |
| 78 | </h1> |
| 79 | |
| 80 | <p className="leading-[1.6] text-[var(--color-text-muted)] text-[var(--text-body)]"> |
| 81 | we're making briven better behind the scenes. your data is safe — we'll be back |
| 82 | shortly. thanks for your patience. |
| 83 | </p> |
| 84 | </div> |
| 85 | |
| 86 | {endLabel ? ( |
| 87 | <p className="font-mono text-[var(--color-text)] text-[var(--text-small)]"> |
| 88 | back around{' '} |
| 89 | <span className="text-[var(--color-primary)]">{endLabel}</span> |
| 90 | </p> |
| 91 | ) : null} |
| 92 | |
| 93 | {customMessage ? ( |
| 94 | <p className="max-w-prose rounded-[var(--radius-md)] border border-[var(--color-border-subtle)] bg-[var(--color-surface)] px-4 py-3 font-mono leading-relaxed text-[var(--color-text-muted)] text-[var(--text-xs)]"> |
| 95 | {customMessage} |
| 96 | </p> |
| 97 | ) : null} |
| 98 | |
| 99 | <p className="font-mono text-[var(--color-text-subtle)] text-[var(--text-xs)]"> |
| 100 | status updates:{' '} |
| 101 | <a |
| 102 | href="https://docs.briven.tech/status" |
| 103 | className="text-[var(--color-text-link)] underline-offset-2 hover:underline" |
| 104 | > |
| 105 | docs.briven.tech/status |
| 106 | </a> |
| 107 | </p> |
| 108 | </div> |
| 109 | |
| 110 | <footer className="mt-12 font-mono text-[10px] text-[var(--color-text-subtle)]"> |
| 111 | built with <span className="text-[#e8344a]">♥</span> in Flanders · flndrn Limited |
| 112 | </footer> |
| 113 | </main> |
| 114 | ); |
| 115 | } |