feat(web): dashboard, wizard, server detail, WS build stream, install snippets
This commit is contained in:
129
apps/web/app/(dashboard)/dashboard/page.tsx
Normal file
129
apps/web/app/(dashboard)/dashboard/page.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { StatusPill } from '@/components/status-pill';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
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 [err, setErr] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<{ servers: ServerRow[] }>('/v1/servers')
|
||||
.then((r) => setServers(r.servers))
|
||||
.catch((e) => setErr((e as Error).message));
|
||||
}, []);
|
||||
|
||||
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 className="flex items-baseline justify-between">
|
||||
<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>
|
||||
<Link
|
||||
href="/servers/new"
|
||||
className="inline-flex h-8 items-center gap-2 rounded-md bg-[--color-accent] px-3 text-[13px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8]"
|
||||
>
|
||||
+ New server
|
||||
</Link>
|
||||
</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="0" sub="of 100,000" />
|
||||
<Card label="Plan" value="Hobby" sub="Upgrade in Settings" />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user