buildmymcpserver/apps/web/app/(dashboard)/dashboard/page.tsx
Marco Sadjadi 089074d104 feat(conversion): Google-first login, truthful dashboard, SSR marketplace, template seeding
- login: OAuth/email on top, phone collapsed behind 'Sign in with phone
  instead' (prod providers: google on, email off, sms on — a developer
  should never see a phone field as the front door)
- dashboard: plan card wired to GET /v1/billing/status; calls card shows
  '—' + pointer to per-server metrics (no user-facing usage endpoint
  exists; previous card showed invented '0 of 100,000 / Hobby')
- templates: server-rendered grid (revalidate 300) via fetchPublicTemplates,
  client browser hydrates with initial data; inviting empty state with
  labeled starter ideas instead of 'No templates yet'
- servers/new: removed upgrade nag from first analyze wait
- scripts/seed-templates.mjs: idempotent dry-run-by-default seeder driving
  the real preview->create->live->publish flow for 6 first-party templates

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXUwmPVRTD8AKQtio6gCN5
2026-07-08 23:00:25 +02:00

144 lines
5.1 KiB
TypeScript

'use client';
import { StatusPill } from '@/components/status-pill';
import { Button } from '@/components/ui/button';
import { apiFetch } from '@/lib/api';
import Link from 'next/link';
import { useEffect, useState } from 'react';
interface ServerRow {
id: string;
name: string;
slug: string;
status: string;
publicUrl: string | null;
createdAt: string;
}
export default function Overview() {
const [servers, setServers] = useState<ServerRow[] | null>(null);
const [plan, setPlan] = useState<string | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
apiFetch<{ servers: ServerRow[] }>('/v1/servers')
.then((r) => setServers(r.servers))
.catch((e) => setErr((e as Error).message));
// Real plan from billing status — never render a hardcoded tier.
apiFetch<{ plan: string }>('/v1/billing/status')
.then((r) => setPlan(r.plan))
.catch(() => setPlan(null));
}, []);
if (err?.includes('401')) {
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
return null;
}
const total = servers?.length ?? 0;
const live = servers?.filter((s) => s.status === 'live').length ?? 0;
return (
<div className="mx-auto max-w-7xl px-6 py-8">
<div>
<h1 className="text-[22px] font-semibold tracking-tight">Overview</h1>
<p className="mt-1 text-[13px] text-[--color-fg-muted]">
Your MCP servers, calls and recent builds.
</p>
</div>
<div className="mt-6 grid gap-3 md:grid-cols-3">
<Card label="Servers" value={total.toString()} sub={`${live} live`} />
<Card
label="Calls this period"
value="—"
sub="Per-server metrics live on each server page"
/>
<Card
label="Plan"
value={plan ? plan.charAt(0).toUpperCase() + plan.slice(1) : '—'}
sub="Manage in Settings → Billing"
/>
</div>
<div className="mt-10">
<div className="flex items-center justify-between">
<h2 className="text-[14px] font-semibold tracking-tight">Recent servers</h2>
<Link
href="/servers"
className="text-[12px] text-[--color-fg-muted] hover:text-[--color-fg]"
>
View all
</Link>
</div>
<div className="panel mt-3">
{servers === null && (
<div className="px-4 py-3 text-[12.5px] text-[--color-fg-muted]">Loading</div>
)}
{servers && servers.length === 0 && (
<div className="px-4 py-12 text-center">
<p className="text-[14px] text-[--color-fg]">No servers yet.</p>
<p className="mt-1 text-[12.5px] text-[--color-fg-muted]">
Describe the tool you want we host the server.
</p>
<Link href="/servers/new" className="mt-4 inline-block">
<Button variant="primary" size="md">
Create your first server
</Button>
</Link>
</div>
)}
{servers && servers.length > 0 && (
<table className="w-full text-[12.5px]">
<thead className="border-b border-[--color-border] text-[--color-fg-subtle]">
<tr>
<th className="px-4 py-2 text-left font-medium">Name</th>
<th className="px-4 py-2 text-left font-medium">Slug</th>
<th className="px-4 py-2 text-left font-medium">Status</th>
<th className="px-4 py-2 text-left font-medium">URL</th>
</tr>
</thead>
<tbody>
{servers.slice(0, 5).map((s) => (
<tr
key={s.id}
className="border-b border-[--color-border] last:border-0 hover:bg-[--color-bg-subtle]"
>
<td className="px-4 py-2.5">
<Link
href={`/servers/${s.id}`}
className="font-medium hover:text-[--color-accent]"
>
{s.name}
</Link>
</td>
<td className="mono px-4 py-2.5 text-[--color-fg-muted]">{s.slug}</td>
<td className="px-4 py-2.5">
<StatusPill status={s.status as never} />
</td>
<td className="mono px-4 py-2.5 text-[--color-fg-muted]">
{s.publicUrl ?? '—'}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
</div>
);
}
function Card({ label, value, sub }: { label: string; value: string; sub: string }) {
return (
<div className="panel p-4">
<div className="text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">{label}</div>
<div className="mt-1.5 text-[24px] font-semibold tracking-tight">{value}</div>
<div className="mt-1 text-[12px] text-[--color-fg-muted]">{sub}</div>
</div>
);
}