page.tsx208 lines · main
1import Link from 'next/link';
2
3import { apiJson } from '../../../../lib/api';
4import { ProjectsList } from './projects-list';
5
6interface Project {
7 id: string;
8 slug: string;
9 name: string;
10 region: string;
11 tier: 'free' | 'pro' | 'team';
12 createdAt: string;
13 orgName: string | null;
14 orgPersonal: boolean | null;
15}
16
17interface PendingInvitation {
18 id: string;
19 projectName: string;
20}
21
22interface PendingOrgInvitation {
23 id: string;
24 orgId: string;
25 orgName: string;
26}
27
28export const dynamic = 'force-dynamic';
29
30export default async function ProjectsPage() {
31 // Fetch projects + pending invites in parallel. Invites failure is
32 // non-fatal — the banner just doesn't render — so we swallow the error
33 // here rather than blocking the projects view.
34 const [data, invitesResult, orgInvitesResult] = await Promise.all([
35 apiJson<{ projects: Project[] }>('/v1/projects'),
36 apiJson<{ invitations: PendingInvitation[] }>('/v1/me/invitations').catch(() => ({
37 invitations: [] as PendingInvitation[],
38 })),
39 apiJson<{ invitations: PendingOrgInvitation[] }>('/v1/me/org-invitations').catch(() => ({
40 invitations: [] as PendingOrgInvitation[],
41 })),
42 ]);
43 const projects = data.projects;
44 const invitations = invitesResult.invitations;
45 const orgInvitations = orgInvitesResult.invitations;
46
47 return (
48 <section>
49 {invitations.length > 0 ? (
50 <Link
51 href="/dashboard/invitations"
52 className="mb-6 flex items-center justify-between rounded-md border border-[var(--color-primary)] bg-[var(--color-surface)] px-4 py-3 transition hover:bg-[var(--color-surface-raised)]"
53 >
54 <div>
55 <p className="font-mono text-sm text-[var(--color-text)]">
56 {invitations.length === 1
57 ? `you have a pending invitation to ${invitations[0]?.projectName ?? 'a project'}.`
58 : `you have ${invitations.length} pending invitations.`}
59 </p>
60 <p className="mt-0.5 font-mono text-xs text-[var(--color-text-muted)]">
61 click to review and accept.
62 </p>
63 </div>
64 <span className="font-mono text-sm text-[var(--color-primary)]">→</span>
65 </Link>
66 ) : null}
67
68 {orgInvitations.length > 0 ? (
69 <Link
70 href="/dashboard/teams"
71 className="mb-6 flex items-center justify-between rounded-md border border-[var(--color-primary)] bg-[var(--color-surface)] px-4 py-3 transition hover:bg-[var(--color-surface-raised)]"
72 >
73 <div>
74 <p className="font-mono text-sm text-[var(--color-text)]">
75 {orgInvitations.length === 1
76 ? `you have a pending invitation to join ${orgInvitations[0]?.orgName ?? 'a team'}.`
77 : `you have ${orgInvitations.length} pending team invitations.`}
78 </p>
79 <p className="mt-0.5 font-mono text-xs text-[var(--color-text-muted)]">
80 open the link in the email to accept.
81 </p>
82 </div>
83 <span className="font-mono text-sm text-[var(--color-primary)]">→</span>
84 </Link>
85 ) : null}
86
87 <header className="mb-8 flex items-center justify-between">
88 <div>
89 <h1 className="font-mono text-xl tracking-tight">projects</h1>
90 <p className="mt-1 font-mono text-sm text-[var(--color-text-muted)]">
91 {projects.length === 0
92 ? 'no projects yet. create one to get started.'
93 : `${projects.length} project${projects.length === 1 ? '' : 's'}`}
94 </p>
95 </div>
96 <Link
97 href="/dashboard/projects/new"
98 className="rounded-md bg-[var(--color-primary)] px-4 py-2 font-mono text-sm font-medium text-[var(--color-text-inverse)] transition hover:bg-[var(--color-primary-hover)]"
99 >
100 new project
101 </Link>
102 </header>
103
104 {projects.length === 0 ? (
105 <div className="rounded-md border border-dashed border-[var(--color-border)] p-8">
106 <div className="flex flex-col gap-6">
107 <div>
108 <p className="font-mono text-sm text-[var(--color-text)]">
109 welcome to briven.
110 </p>
111 <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]">
112 a project is one postgres schema + one function runtime + one set of deploy
113 keys. you&rsquo;ll usually have one per app you ship.
114 </p>
115 </div>
116
117 <ol className="flex flex-col gap-4 font-mono text-xs">
118 <li>
119 <p className="text-[var(--color-text)]">1 · create your first project</p>
120 <p className="mt-1 text-[var(--color-text-muted)]">
121 click <em>new project</em> above. takes ~3 seconds — postgres schema +
122 runtime spin up in the background.
123 </p>
124 </li>
125 <li>
126 <p className="text-[var(--color-text)]">2 · build the schema · two paths</p>
127 <p className="mt-1 text-[var(--color-text-muted)]">
128 <strong>dashboard</strong> — open the new project&apos;s <em>studio</em> tab,
129 click <em>+ new table</em>, define columns, set PKs/FKs/indexes. ideal for
130 prototyping and small one-off changes.
131 </p>
132 <p className="mt-2 text-[var(--color-text-muted)]">
133 <strong>cli + git</strong> — sign in and wire this folder:
134 </p>
135 <pre className="mt-1 overflow-x-auto rounded-sm border border-[var(--color-border-subtle)] bg-[var(--color-code-bg)] px-3 py-2 text-[var(--color-code-text)]">
136 <code>{`mkdir my-app && cd my-app
137npx @briven/cli setup --name my-app # NEW project
138# or: briven connect p_… # EXISTING project
139briven deploy`}</code>
140 </pre>
141 <p className="mt-1 text-[var(--color-text-muted)]">
142 never <code>setup --project</code> — that flag is gone; use{' '}
143 <code>briven connect</code>. optional starter files:{' '}
144 <code>--template todo-app|chat|blank</code> (templates are not the product).
145 studio has <em>copy as schema.ts</em> to graduate to git.
146 </p>
147 <details className="mt-2 text-[10px] text-[var(--color-text-subtle)]">
148 <summary className="cursor-pointer">manual key path (no browser OAuth)</summary>
149 <pre className="mt-1 overflow-x-auto rounded-sm border border-[var(--color-border-subtle)] bg-[var(--color-code-bg)] px-3 py-2 text-[var(--color-code-text)]">
150 <code>
151 {`npx @briven/cli init --template todo-app
152briven login --project <p_id> --key <brk_key>
153briven link
154briven deploy`}
155 </code>
156 </pre>
157 </details>
158 <details className="mt-2 text-[10px] text-[var(--color-text-subtle)]">
159 <summary className="cursor-pointer">offline install (curl, no npm)</summary>
160 <pre className="mt-1 overflow-x-auto rounded-sm border border-[var(--color-border-subtle)] bg-[var(--color-code-bg)] px-3 py-2 text-[var(--color-code-text)]">
161 <code>{`curl -fsSL https://briven.tech/install | sh
162cd my-app && briven setup`}</code>
163 </pre>
164 </details>
165 </li>
166 <li>
167 <p className="text-[var(--color-text)]">3 · invoke + iterate</p>
168 <p className="mt-1 text-[var(--color-text-muted)]">
169 <code>briven invoke listTodos</code> to test from the cli, or wire{' '}
170 <code>@briven/react</code> /{' '}
171 <code>@briven/svelte</code> /{' '}
172 <code>@briven/vue</code> into your frontend. queries are reactive by
173 default — they re-run when the underlying rows change.
174 </p>
175 </li>
176 </ol>
177
178 <div className="flex flex-wrap gap-3 font-mono text-xs">
179 <a
180 href="https://docs.briven.tech/quickstart"
181 className="underline underline-offset-2 hover:text-[var(--color-text)]"
182 >
183 full quickstart →
184 </a>
185 <a
186 href="https://docs.briven.tech/cli"
187 className="text-[var(--color-text-muted)] underline underline-offset-2 hover:text-[var(--color-text)]"
188 >
189 cli reference
190 </a>
191 <a
192 href="https://docs.briven.tech/functions"
193 className="text-[var(--color-text-muted)] underline underline-offset-2 hover:text-[var(--color-text)]"
194 >
195 functions
196 </a>
197 </div>
198 </div>
199 </div>
200 ) : (
201 <ProjectsList
202 projects={projects}
203 apiOrigin={process.env.NEXT_PUBLIC_BRIVEN_API_ORIGIN ?? ''}
204 />
205 )}
206 </section>
207 );
208}