page.tsx82 lines · main
1import { redirect } from 'next/navigation';
2import { revalidatePath } from 'next/cache';
3
4import { apiFetch, apiJson } from '../../../../lib/api';
5import { AcceptButton } from './accept-button';
6
7interface PendingInvitation {
8 id: string;
9 projectId: string;
10 projectName: string;
11 role: string;
12 invitedBy: string | null;
13 expiresAt: string;
14}
15
16export const metadata = { title: 'invitations' };
17export const dynamic = 'force-dynamic';
18
19export default async function InvitationsPage() {
20 const data = await apiJson<{ invitations: PendingInvitation[] }>('/v1/me/invitations');
21 const invitations = data.invitations;
22
23 async function accept(invitationId: string): Promise<void> {
24 'use server';
25 const res = await apiFetch(`/v1/me/invitations/${invitationId}/accept`, {
26 method: 'POST',
27 headers: { 'content-type': 'application/json' },
28 });
29 if (!res.ok) {
30 const body = (await res.json().catch(() => ({}))) as { message?: string };
31 throw new Error(body.message ?? `accept failed: ${res.status}`);
32 }
33 const result = (await res.json()) as { projectId: string };
34 revalidatePath('/dashboard/invitations');
35 redirect(`/dashboard/projects/${result.projectId}`);
36 }
37
38 return (
39 <section>
40 <header className="mb-8">
41 <h1 className="font-mono text-xl tracking-tight">invitations</h1>
42 <p className="mt-1 font-mono text-sm text-[var(--color-text-muted)]">
43 {invitations.length === 0
44 ? 'no pending invitations.'
45 : `${invitations.length} pending invitation${invitations.length === 1 ? '' : 's'}`}
46 </p>
47 </header>
48
49 {invitations.length === 0 ? (
50 <div className="rounded-md border border-dashed border-[var(--color-border)] p-10 text-center">
51 <p className="font-mono text-sm text-[var(--color-text-muted)]">
52 project owners can invite you by email. when one does, the invitation will appear here.
53 </p>
54 </div>
55 ) : (
56 <ul className="flex flex-col gap-2">
57 {invitations.map((invite) => {
58 const expires = new Date(invite.expiresAt);
59 const expiresIn = Math.max(
60 0,
61 Math.ceil((expires.getTime() - Date.now()) / 86_400_000),
62 );
63 return (
64 <li
65 key={invite.id}
66 className="flex items-center justify-between rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] px-4 py-3"
67 >
68 <div>
69 <p className="font-mono text-sm">{invite.projectName}</p>
70 <p className="mt-0.5 font-mono text-xs text-[var(--color-text-subtle)]">
71 role: {invite.role} · expires in {expiresIn} day{expiresIn === 1 ? '' : 's'}
72 </p>
73 </div>
74 <AcceptButton invitationId={invite.id} action={accept} />
75 </li>
76 );
77 })}
78 </ul>
79 )}
80 </section>
81 );
82}