feat(marketplace): dashboard nav link + My-templates filter

The logged-in user can now reach the marketplace and filter to their own
templates.

Dashboard nav:
- Added 'Marketplace' item (Overview · Servers · Marketplace · Audit · Settings).

/templates page — login-aware:
- Detects session via /v1/auth/me. Logged-in users get a 'Dashboard' + '+ New
  server' header instead of 'Home' + 'Start building'.
- New [All templates | My templates] scope toggle, shown only when logged in.
- 'My templates' loads GET /v1/templates/mine and shows EVERY status the user
  owns (public / hidden / draft / takedown) with a colored status badge on each
  card — so a template you unshared doesn't appear to have vanished.
- Sort tabs (trending/top/newest) hide in 'mine' scope — meaningless for a
  handful of own templates. Category filter + search still apply (client-side).
- Takedown cards link to the source server's Publish tab instead of the detail
  route (which 410s); everything else opens the detail page.

Backend:
- GET /v1/templates/mine (requireAuth) — all own templates, any status,
  registered before /:slug so the static route always wins the match.
- GET /v1/templates/:slug — now does an optional session check: the OWNER can
  view their own hidden/draft template (so a 'My templates' card click never
  dead-ends in a 404). takedown stays 410 for everyone, owner included — that's
  an admin decision, not the owner's to reverse.

Detail page:
- Fork CTA is gated on status === 'public'. For a non-public template the owner
  sees an amber 'not forkable — re-share from the Publish tab' notice plus a
  'Manage in server' link, instead of a Fork button that would fail silently.

Verified:
- GET /v1/templates/mine → marco's 1 template; 401 without auth
- Owner GET of a hidden template → 200 status:hidden; anon → 404
- Dashboard nav shows Marketplace (screenshot)
- /templates 'My templates' toggle → only own template, public badge, sort tabs
  hidden (screenshot)
This commit is contained in:
Marco Sadjadi
2026-05-20 17:18:58 +02:00
parent a189111782
commit 414903f16d
4 changed files with 276 additions and 102 deletions

View File

@@ -28,6 +28,7 @@ interface TemplateDetail {
shortDescription: string;
longDescription: string | null;
category: string;
status: 'draft' | 'public' | 'hidden' | 'takedown';
verified: boolean;
forkCount: number;
activeDeployments: number;
@@ -37,6 +38,7 @@ interface TemplateDetail {
scopes: string[];
ownerName: string | null;
ownerOrgName: string | null;
sourceServerId: string | null;
createdAt: string;
}
@@ -208,12 +210,31 @@ export default function TemplateDetail() {
<aside className="space-y-3">
<div className="panel p-4">
<Button variant="primary" size="lg" className="w-full" onClick={useTemplate}>
Fork this template
</Button>
<p className="mt-2 text-[11.5px] text-[--color-fg-muted]">
One click your own isolated container.
</p>
{template.status === 'public' ? (
<>
<Button variant="primary" size="lg" className="w-full" onClick={useTemplate}>
Fork this template
</Button>
<p className="mt-2 text-[11.5px] text-[--color-fg-muted]">
One click your own isolated container.
</p>
</>
) : (
<>
<div className="rounded-md border border-amber-400/30 bg-amber-400/5 p-2.5 text-[12px] text-amber-200/90">
This template is <span className="mono">{template.status}</span> not
forkable. {template.sourceServerId ? 'Re-share it from the servers Publish tab to allow forks.' : ''}
</div>
{template.sourceServerId && (
<a
href={`/servers/${template.sourceServerId}`}
className="mt-2 inline-flex h-8 w-full items-center justify-center rounded-md border border-[--color-border] bg-[--color-bg-elevated] text-[12.5px] text-[--color-fg] transition-colors hover:bg-[--color-bg-subtle]"
>
Manage in server
</a>
)}
</>
)}
</div>
<div className="panel p-3 text-[12px]">