page.tsx108 lines · main
1import { redirect } from 'next/navigation';
2
3import { apiFetch, apiJson } from '../../../../../lib/api';
4
5export const dynamic = 'force-dynamic';
6export const metadata = { title: 'new team' };
7
8interface SubscriptionSummary {
9 tier: 'free' | 'pro' | 'team';
10}
11
12async function createTeam(formData: FormData) {
13 'use server';
14 const name = String(formData.get('name') ?? '').trim();
15 const slug = String(formData.get('slug') ?? '').trim() || undefined;
16 if (!name) {
17 throw new Error('team name is required');
18 }
19 const res = await apiFetch('/v1/orgs', {
20 method: 'POST',
21 headers: { 'content-type': 'application/json' },
22 body: JSON.stringify({ name, slug }),
23 });
24 if (!res.ok) {
25 const body = await res.text().catch(() => '');
26 if (res.status === 402) {
27 redirect('/dashboard/billing');
28 }
29 throw new Error(`team create failed (${res.status}): ${body}`);
30 }
31 redirect('/dashboard/teams');
32}
33
34export default async function NewTeamPage() {
35 const subscription = await apiJson<SubscriptionSummary>('/v1/billing/subscription').catch(() => ({
36 tier: 'free' as const,
37 }));
38 if (subscription.tier === 'free') {
39 return (
40 <section className="max-w-lg">
41 <header className="mb-8">
42 <h1 className="font-mono text-xl tracking-tight">teams are a paid feature</h1>
43 <p className="mt-1 font-mono text-sm text-[var(--color-text-muted)]">
44 on the free tier you get one personal org. upgrade to pro or team to spin up
45 unlimited shared workspaces with invited collaborators.
46 </p>
47 </header>
48 <a
49 href="/dashboard/billing"
50 className="inline-block 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)]"
51 >
52 see upgrade options
53 </a>
54 </section>
55 );
56 }
57
58 return (
59 <section className="max-w-lg">
60 <header className="mb-8">
61 <h1 className="font-mono text-xl tracking-tight">new team</h1>
62 <p className="mt-1 font-mono text-sm text-[var(--color-text-muted)]">
63 a team org is a separate workspace from your personal org. projects, billing, and
64 membership are scoped to it. you can be in many teams; you can also rename a team
65 later from its settings.
66 </p>
67 </header>
68
69 <form action={createTeam} className="flex flex-col gap-5">
70 <label className="flex flex-col gap-2">
71 <span className="font-mono text-xs text-[var(--color-text-muted)]">team name</span>
72 <input
73 name="name"
74 type="text"
75 required
76 maxLength={200}
77 placeholder="acme inc"
78 className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 font-mono text-sm outline-none focus:border-[var(--color-primary)]"
79 />
80 </label>
81
82 <label className="flex flex-col gap-2">
83 <span className="font-mono text-xs text-[var(--color-text-muted)]">
84 slug{' '}
85 <span className="text-[var(--color-text-subtle)]">(optional — generated if blank)</span>
86 </span>
87 <input
88 name="slug"
89 type="text"
90 pattern="[a-z0-9](?:[a-z0-9\-]{0,38}[a-z0-9])?"
91 maxLength={40}
92 placeholder="acme"
93 className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 font-mono text-sm outline-none focus:border-[var(--color-primary)]"
94 />
95 </label>
96
97 <div className="flex gap-3">
98 <button
99 type="submit"
100 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)]"
101 >
102 create team
103 </button>
104 </div>
105 </form>
106 </section>
107 );
108}