page.tsx179 lines · main
1import Link from 'next/link';
2
3import { apiJson } from '../../../../lib/api';
4import { RowDeleteMigrationButton } from './row-delete-migration-button';
5
6export const metadata = { title: 'my migrations' };
7export const dynamic = 'force-dynamic';
8
9interface CustomerRequest {
10 id: string;
11 source: string;
12 sourceUrl: string | null;
13 sourceNotes: string;
14 estimatedTables: number | null;
15 estimatedRows: string | null;
16 estimatedFunctions: number | null;
17 urgency: string;
18 status: string;
19 contactEmail: string;
20 createdAt: string;
21 updatedAt: string;
22}
23
24const STATUS_BLURB: Record<string, string> = {
25 new: 'queued — we’ll be in touch within one business day.',
26 contacted: 'we’ve reached out — check your inbox.',
27 scheduled: 'a migration window is scheduled.',
28 in_progress: 'we’re moving your project right now.',
29 completed: 'migration completed.',
30 cancelled: 'request cancelled.',
31};
32
33function statusTone(status: string): string {
34 if (status === 'completed') {
35 return 'border-[var(--color-primary)] bg-[var(--color-primary-subtle)] text-[var(--color-primary)]';
36 }
37 if (status === 'cancelled') {
38 return 'border-[var(--color-border-subtle)] text-[var(--color-text-subtle)]';
39 }
40 if (status === 'in_progress') {
41 return 'border-[var(--color-warning)] text-[var(--color-warning)]';
42 }
43 return 'border-[var(--color-border)] text-[var(--color-text-muted)]';
44}
45
46function formatTime(iso: string): string {
47 return new Date(iso).toISOString().replace('T', ' ').slice(0, 16) + ' utc';
48}
49
50export default async function MyMigrationsPage() {
51 const { requests } = await apiJson<{ requests: CustomerRequest[] }>(
52 '/v1/migration-requests',
53 );
54
55 const open = requests.filter(
56 (r) => r.status !== 'completed' && r.status !== 'cancelled',
57 );
58 const closed = requests.filter(
59 (r) => r.status === 'completed' || r.status === 'cancelled',
60 );
61
62 return (
63 <section className="flex flex-col gap-6">
64 <header>
65 <h1 className="font-mono text-xl tracking-tight">my migrations</h1>
66 <p className="mt-1 font-mono text-sm text-[var(--color-text-muted)]">
67 every migration request you’ve submitted. status updates from us land here as we
68 progress through the move. for now you’ll also get email updates at the contact
69 address on each request.
70 </p>
71 </header>
72
73 {requests.length === 0 ? (
74 <div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-6">
75 <p className="font-mono text-sm text-[var(--color-text)]">
76 no migration requests yet.
77 </p>
78 <p className="mt-2 font-mono text-xs text-[var(--color-text-muted)]">
79 coming from convex, supabase, firebase or somewhere else? start the wizard and
80 we’ll handle the move for free during beta.
81 </p>
82 <Link
83 href="/dashboard/projects/new"
84 className="mt-4 inline-flex rounded-md bg-[var(--color-primary)] px-3 py-1.5 font-mono text-xs text-[var(--color-text-inverse)] hover:bg-[var(--color-primary-hover)]"
85 >
86 start a migration
87 </Link>
88 </div>
89 ) : (
90 <>
91 {open.length > 0 ? (
92 <div className="flex flex-col gap-3">
93 <h2 className="font-mono text-xs uppercase tracking-wider text-[var(--color-text-subtle)]">
94 open · {open.length}
95 </h2>
96 {open.map((r) => (
97 <RequestCard key={r.id} request={r} />
98 ))}
99 </div>
100 ) : null}
101
102 {closed.length > 0 ? (
103 <div className="flex flex-col gap-3">
104 <h2 className="font-mono text-xs uppercase tracking-wider text-[var(--color-text-subtle)]">
105 done · {closed.length}
106 </h2>
107 {closed.map((r) => (
108 <RequestCard key={r.id} request={r} />
109 ))}
110 </div>
111 ) : null}
112 </>
113 )}
114
115 <p className="mt-4 font-mono text-xs text-[var(--color-text-subtle)]">
116 question about an in-flight migration? email{' '}
117 <a
118 href="mailto:migrations@flndrn.com"
119 className="underline underline-offset-2 hover:text-[var(--color-text-muted)]"
120 >
121 migrations@flndrn.com
122 </a>{' '}
123 and quote the request id.
124 </p>
125 </section>
126 );
127}
128
129function RequestCard({ request }: { request: CustomerRequest }) {
130 return (
131 <article className="group flex flex-col gap-3 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] px-4 py-3 transition hover:border-[var(--color-border-strong)]">
132 <div className="flex flex-wrap items-center gap-2">
133 <span
134 className={`rounded-full border px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-wider ${statusTone(request.status)}`}
135 >
136 {request.status.replace(/_/g, ' ')}
137 </span>
138 <span className="rounded-full border border-[var(--color-border-subtle)] px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-wider text-[var(--color-text-subtle)]">
139 {request.source}
140 </span>
141 <span className="font-mono text-[10px] text-[var(--color-text-subtle)]">
142 submitted {formatTime(request.createdAt)}
143 </span>
144 <Link
145 href={`/dashboard/migrations/${request.id}`}
146 className="ml-auto font-mono text-[10px] text-[var(--color-text-subtle)] hover:text-[var(--color-text)] group-hover:text-[var(--color-text-muted)]"
147 >
148 {request.id} →
149 </Link>
150 <RowDeleteMigrationButton requestId={request.id} />
151 </div>
152
153 <p className="font-mono text-sm text-[var(--color-text)]">
154 {STATUS_BLURB[request.status] ?? 'in progress.'}
155 </p>
156
157 <div className="flex flex-wrap gap-4 font-mono text-[10px] text-[var(--color-text-subtle)]">
158 <span>urgency: {request.urgency.replace(/_/g, ' ')}</span>
159 <span>tables: {request.estimatedTables ?? '—'}</span>
160 <span>rows: {request.estimatedRows ?? '—'}</span>
161 <span>functions: {request.estimatedFunctions ?? '—'}</span>
162 <span>contact: {request.contactEmail}</span>
163 </div>
164
165 {request.sourceUrl ? (
166 <p className="font-mono text-[10px] text-[var(--color-text-muted)]">
167 source URL: <code>{request.sourceUrl}</code>
168 </p>
169 ) : null}
170
171 {request.sourceNotes ? (
172 <details className="font-mono text-xs text-[var(--color-text-muted)]">
173 <summary className="cursor-pointer">your notes</summary>
174 <pre className="mt-1 whitespace-pre-wrap break-words">{request.sourceNotes}</pre>
175 </details>
176 ) : null}
177 </article>
178 );
179}