page.tsx114 lines · main
1import { MailIcon } from '@/components/ui/mail';
2import { UsersIcon } from '@/components/ui/users';
3
4import { apiJson } from '@/lib/api';
5import { toValidDate } from '@/lib/utils';
6
7import { EmptyState } from '../_components/empty-state';
8import { Section } from '../_components/section';
9import { AllowlistAddForm, AllowlistRemoveButton } from './allowlist-controls';
10
11interface Entry {
12 id: string;
13 email: string;
14 invitedBy: string | null;
15 invitedAt: string;
16 acceptedAt: string | null;
17 notes: string | null;
18}
19
20interface LaunchStatus {
21 openSignups: boolean;
22}
23
24export const dynamic = 'force-dynamic';
25
26function publicApiOrigin(): string {
27 return process.env.NEXT_PUBLIC_BRIVEN_API_ORIGIN ?? '';
28}
29
30export default async function AllowlistPage() {
31 const [entriesResult, launchResult] = await Promise.all([
32 apiJson<{ entries: Entry[] }>('/v1/admin/signup-allowlist'),
33 apiJson<LaunchStatus>('/v1/admin/launch-status').catch(() => null),
34 ]);
35 const entries = entriesResult.entries;
36 const openSignups = launchResult?.openSignups ?? false;
37 const apiOrigin = publicApiOrigin();
38
39 return (
40 <div className="flex flex-col gap-10">
41 <header className="flex flex-col gap-2">
42 <div className="flex items-center gap-2">
43 <span className="text-[var(--color-primary)]">
44 <UsersIcon size={20} />
45 </span>
46 <h1 className="font-mono text-xl tracking-tight">signup allowlist</h1>
47 </div>
48 <p className="max-w-prose font-mono text-sm text-[var(--color-text-muted)]">
49 while <code>BRIVEN_OPEN_SIGNUPS</code> is{' '}
50 <span
51 className={
52 openSignups ? 'text-[var(--color-primary)]' : 'text-[var(--color-warning)]'
53 }
54 >
55 {openSignups ? 'true (open)' : 'false (invite-only)'}
56 </span>
57 , only emails on this list can sign up. mutations require fresh step-up auth — the
58 prompt appears inline on stale sessions.
59 </p>
60 </header>
61
62 <AllowlistAddForm apiOrigin={apiOrigin} />
63
64 <Section title={`entries · ${entries.length}`} icon={<MailIcon size={16} />}>
65 {entries.length === 0 ? (
66 <EmptyState
67 icon={<MailIcon size={28} />}
68 title="no allowlist entries yet"
69 message="add the first email above to invite someone into the beta."
70 />
71 ) : (
72 <ul className="flex flex-col divide-y divide-[var(--color-border-subtle)] rounded-xl border border-[var(--color-border-subtle)] bg-[var(--color-surface)]">
73 {entries.map((e) => (
74 <li
75 key={e.id}
76 className="flex flex-col gap-3 px-6 py-4 sm:flex-row sm:items-center sm:justify-between"
77 >
78 <div className="min-w-0">
79 <div className="flex flex-wrap items-baseline gap-2">
80 <span className="font-mono text-sm text-[var(--color-text)]">{e.email}</span>
81 {e.acceptedAt ? (
82 <span className="rounded-full border border-[var(--color-border-primary)] bg-[var(--color-primary-subtle)] px-2 py-0.5 font-mono text-[9px] uppercase tracking-wider text-[var(--color-primary)]">
83 claimed
84 </span>
85 ) : (
86 <span className="rounded-full border border-[var(--color-warning)] px-2 py-0.5 font-mono text-[9px] uppercase tracking-wider text-[var(--color-warning)]">
87 pending
88 </span>
89 )}
90 </div>
91 <p className="mt-1.5 font-mono text-[11px] text-[var(--color-text-subtle)]">
92 invited {formatTimestamp(e.invitedAt)}
93 {e.acceptedAt ? ` · claimed ${formatTimestamp(e.acceptedAt)}` : ''}
94 </p>
95 {e.notes ? (
96 <p className="mt-1.5 font-mono text-xs text-[var(--color-text-muted)]">
97 {e.notes}
98 </p>
99 ) : null}
100 </div>
101 <AllowlistRemoveButton email={e.email} apiOrigin={apiOrigin} />
102 </li>
103 ))}
104 </ul>
105 )}
106 </Section>
107 </div>
108 );
109}
110
111function formatTimestamp(iso: string): string {
112 const d = toValidDate(iso);
113 return d ? d.toISOString().replace('T', ' ').slice(0, 16) + ' utc' : '—';
114}