// app/vault/page.tsx "use client"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; import { useApi } from "@/lib/api"; type BuildDto = { id: string; // internal numeric id (not used by UI) uuid: string; // public identifier title: string; description?: string | null; isPublic?: boolean | null; createdAt?: string | null; updatedAt?: string | null; }; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ""; // UUID validator (defensive) const isUuid = (v: string) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( v ); function fmt(d?: string | null) { if (!d) return "—"; const dt = new Date(d); if (Number.isNaN(dt.getTime())) return d; return dt.toLocaleString(); } export default function VaultPage() { const router = useRouter(); const api = useApi(); // NOTE: // - `loading` here is AuthContext hydration, not network. // - `token` is the real gate for /me endpoints. const { user, token, loading: authLoading } = useAuth(); const [builds, setBuilds] = useState([]); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const authed = !!user && !!token; // Redirect if not logged in (after auth hydrates) useEffect(() => { if (!authLoading && !authed) { router.replace("/login"); } }, [authLoading, authed, router]); // When auth identity changes, clear stale list to avoid “ghost builds” useEffect(() => { if (authLoading) return; setBuilds([]); setError(null); }, [authLoading, user?.uuid, token]); // Fetch user builds useEffect(() => { if (authLoading || !authed) return; let cancelled = false; (async () => { setBusy(true); setError(null); try { // IMPORTANT: // Use api wrapper so Authorization: Bearer is consistently applied. const res = await api.get("/api/v1/builds/me"); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(text || `Request failed (${res.status})`); } const data = (await res.json()) as BuildDto[]; if (!cancelled) setBuilds(Array.isArray(data) ? data : []); } catch (e: any) { if (!cancelled) setError(e?.message || "Failed to load builds"); } finally { if (!cancelled) setBusy(false); } })(); return () => { cancelled = true; }; }, [authLoading, authed, api]); if (authLoading) { return (
Loading…
); } // Already redirected if (!authed) return null; return (
{/* Header */}

Vault

Your saved builds. Open one to keep tweaking parts, or add details to submit it to the community.

New Build Account →
{/* Error */} {error && (
{error}
)} {/* Builds list */}
My Builds
{busy ? "Refreshing…" : `${builds.length} total`}
{builds.length === 0 ? (
No builds yet. Create one in the builder and hit “Save As…”
) : (
    {builds.map((b) => { const valid = !!b?.uuid && isUuid(String(b.uuid)); const published = Boolean(b.isPublic); const openHref = valid ? `/builder?load=${encodeURIComponent(b.uuid)}` : "#"; const detailsHref = valid ? `/vault/${encodeURIComponent(b.uuid)}/edit` : "#"; return (
  • {b.title}
    {/* Status chip */} {published ? "Published" : "Draft"} {!valid && ( Invalid UUID )}
    Updated: {fmt(b.updatedAt)} • UUID:{" "} {b.uuid}
    {b.description && (
    {b.description}
    )}
    View in Builder View / Edit
  • ); })}
)}
); }