'use client'; import { useEffect, useState } from 'react'; import Link from 'next/link'; import { apiFetch } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { ShieldCheck } from 'lucide-react'; interface AdminTemplate { id: string; slug: string; title: string; shortDescription: string; category: string; status: 'draft' | 'public' | 'hidden' | 'takedown'; verified: boolean; takedownReason: string | null; forkCount: number; activeDeployments: number; ownerEmail: string | null; ownerOrgName: string | null; createdAt: string; } const STATUS_FILTERS = ['', 'public', 'hidden', 'takedown', 'draft']; export default function AdminTemplatesPage() { const [rows, setRows] = useState(null); const [statusFilter, setStatusFilter] = useState(''); async function reload() { const r = await apiFetch<{ templates: AdminTemplate[] }>('/v1/admin/templates'); setRows(r.templates); } useEffect(() => { reload(); }, []); async function toggleVerified(t: AdminTemplate) { if (!confirm(`${t.verified ? 'Unverify' : 'Verify'} "${t.title}"?`)) return; await apiFetch(`/v1/admin/templates/${t.id}`, { method: 'PATCH', body: JSON.stringify({ verified: !t.verified }), }); reload(); } async function takedown(t: AdminTemplate) { if (t.status === 'takedown') { if (!confirm(`Lift takedown on "${t.title}"? Forked servers stay paused — owners must re-deploy.`)) return; await apiFetch(`/v1/admin/templates/${t.id}`, { method: 'PATCH', body: JSON.stringify({ status: 'public', takedownReason: null }), }); } else { const reason = prompt( `Take down "${t.title}"? This pauses ALL ${t.activeDeployments} active fork containers. Reason:`, '', ); if (reason === null) return; await apiFetch(`/v1/admin/templates/${t.id}`, { method: 'PATCH', body: JSON.stringify({ status: 'takedown', takedownReason: reason || 'Removed by admin' }), }); } reload(); } async function toggleHidden(t: AdminTemplate) { const next = t.status === 'public' ? 'hidden' : 'public'; await apiFetch(`/v1/admin/templates/${t.id}`, { method: 'PATCH', body: JSON.stringify({ status: next }), }); reload(); } const visible = rows?.filter((t) => (statusFilter ? t.status === statusFilter : true)); return (

Templates moderation

Verify quality templates, hide low-effort ones, take down anything malicious. Takedowns cascade-pause every fork container.

{rows === null && (

Loading…

)} {visible && visible.length === 0 && (

No templates yet.

)} {visible && visible.length > 0 && ( {visible.map((t) => ( ))}
Title Owner Category Status Stats Actions
{t.title} {t.verified && ( )}
{t.slug}
{t.ownerEmail ?? '—'} {t.category} {t.forkCount} forks · {t.activeDeployments} live
{t.status !== 'takedown' && ( )}
)}
); } function StatusBadge({ status, reason, }: { status: AdminTemplate['status']; reason: string | null; }) { const styles: Record = { public: 'border-emerald-400/40 bg-emerald-400/10 text-emerald-300', hidden: 'border-amber-400/40 bg-amber-400/10 text-amber-300', takedown: 'border-red-400/40 bg-red-400/10 text-red-300', draft: 'border-zinc-400/40 bg-zinc-400/10 text-zinc-300', }; return ( {status} ); }