"use client"; import { Suspense, useState, useEffect, useCallback } from "react"; import { useParams, useSearchParams, useRouter } from "next/navigation"; import { useSession } from "next-auth/react"; import Link from "next/link"; // ── Types ───────────────────────────────────────────────────────────────────── interface AppEntry { name: string; path: string; } interface SurfaceEntry { id: string; label: string; lockedTheme?: string; } interface FileItem { name: string; path: string; type: "file" | "dir" | "symlink"; } interface TreeNode { name: string; path: string; type: "file" | "dir"; children?: TreeNode[]; expanded?: boolean; loaded?: boolean; } // ── Constants ───────────────────────────────────────────────────────────────── const INFRA_ITEMS = [ { id: "builds", label: "Builds", icon: "⬡" }, { id: "databases", label: "Databases", icon: "◫" }, { id: "services", label: "Services", icon: "◎" }, { id: "environment", label: "Environment", icon: "≡" }, { id: "domains", label: "Domains", icon: "◬" }, { id: "logs", label: "Logs", icon: "≈" }, ]; const SURFACE_LABELS: Record = { webapp: "Web App", marketing: "Marketing Site", admin: "Admin Panel", }; const SURFACE_ICONS: Record = { webapp: "◈", marketing: "◌", admin: "◫", }; // ── Language / syntax helpers ───────────────────────────────────────────────── function langFromName(name: string): string { const ext = name.split(".").pop()?.toLowerCase() ?? ""; const map: Record = { ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", json: "json", md: "markdown", css: "css", scss: "css", html: "html", py: "python", sh: "shell", yaml: "yaml", yml: "yaml", toml: "toml", prisma: "prisma", sql: "sql", }; return map[ext] ?? "text"; } function highlightCode(code: string, lang: string): React.ReactNode[] { return code.split("\n").map((line, i) => { const commentPrefixes = ["//", "#", "--"]; if (commentPrefixes.some(p => line.trimStart().startsWith(p))) { return
{line}
; } const kwRe = /\b(import|export|from|const|let|var|function|return|if|else|async|await|type|interface|class|extends|new|default|null|undefined|true|false|void|string|number|boolean|React)\b/g; const parts = line.split(kwRe); const tokens = parts.map((p, j) => { if (!p) return null; if (/^(import|export|from|const|let|var|function|return|if|else|async|await|type|interface|class|extends|new|default|null|undefined|true|false|void|string|number|boolean|React)$/.test(p)) return {p}; return {p}; }); return
{tokens.length ? tokens : "\u00a0"}
; }); } // ── File tree row ───────────────────────────────────────────────────────────── function TreeRow({ node, depth, selectedPath, onSelect, onToggle }: { node: TreeNode; depth: number; selectedPath: string | null; onSelect: (p: string) => void; onToggle: (p: string) => void; }) { const active = selectedPath === node.path; const isDir = node.type === "dir"; const ext = node.name.split(".").pop()?.toLowerCase() ?? ""; const fileColor = ext === "tsx" || ext === "ts" ? "#3178c6" : ext === "jsx" || ext === "js" ? "#f0db4f" : ext === "css" || ext === "scss" ? "#e879f9" : "#b5b0a6"; return ( <> {isDir && node.expanded && node.children?.map(c => )} ); } // ── Left nav shared styles ──────────────────────────────────────────────────── const NAV_GROUP_LABEL: React.CSSProperties = { fontSize: "0.6rem", fontWeight: 700, color: "#b5b0a6", letterSpacing: "0.09em", textTransform: "uppercase", padding: "12px 12px 5px", fontFamily: "Outfit, sans-serif", }; function NavItem({ label, active, onClick, indent = false }: { label: string; active: boolean; onClick: () => void; indent?: boolean }) { return ( ); } // ── Placeholder panel ───────────────────────────────────────────────────────── function Placeholder({ icon, title, desc }: { icon: string; title: string; desc: string }) { return (
{icon}
{title}
{desc}
Coming soon
); } // ── Infra content ───────────────────────────────────────────────────────────── function InfraContent({ tab, projectId, workspace }: { tab: string; projectId: string; workspace: string }) { const base = `/${workspace}/project/${projectId}/infrastructure`; const descriptions: Record = { databases: { icon: "◫", title: "Databases", desc: "PostgreSQL, Redis, and other databases — provisioned and managed with connection strings auto-injected." }, services: { icon: "◎", title: "Services", desc: "Background workers, queues, email delivery, file storage, and third-party integrations." }, environment: { icon: "≡", title: "Environment", desc: "Environment variables and secrets, encrypted at rest and auto-injected into your containers." }, domains: { icon: "◬", title: "Domains", desc: "Custom domains and SSL certificates for all your deployed services." }, logs: { icon: "≈", title: "Logs", desc: "Runtime logs, request traces, and error reports streaming from deployed services." }, builds: { icon: "⬡", title: "Builds", desc: "Deployment history, build logs, and rollback controls for all your apps." }, }; const d = descriptions[tab]; return (
{tab}
Open full view →
{d && }
); } // ── Layouts content ─────────────────────────────────────────────────────────── function LayoutsContent({ surfaces, projectId, workspace, activeSurfaceId, onSelectSurface }: { surfaces: SurfaceEntry[]; projectId: string; workspace: string; activeSurfaceId: string | null; onSelectSurface: (id: string) => void; }) { if (surfaces.length === 0) { return ( ); } const active = surfaces.find(s => s.id === activeSurfaceId) ?? surfaces[0]; return (
Layouts
Edit in Design →
{surfaces.map(s => (
onSelectSurface(s.id)} style={{ background: active?.id === s.id ? "#fff" : "#faf8f5", border: `1px solid ${active?.id === s.id ? "#1a1a1a" : "#e8e4dc"}`, borderRadius: 10, padding: "16px 20px", cursor: "pointer", minWidth: 180, flex: "1 1 180px", maxWidth: 240, transition: "border-color 0.1s", }}>
{SURFACE_LABELS[s.id] ?? s.id}
{s.lockedTheme ? (
Theme: {s.lockedTheme}
) : (
Not configured
)}
))}
Click a surface to select it, then open the Design editor to configure themes, fonts, and components.
); } // ── Code content (file browser) ─────────────────────────────────────────────── function CodeContent({ projectId, appName, rootPath }: { projectId: string; appName: string; rootPath: string }) { const { status } = useSession(); const [tree, setTree] = useState([]); const [treeLoading, setTreeLoading] = useState(false); const [selectedPath, setSelectedPath] = useState(null); const [fileContent, setFileContent] = useState(null); const [fileLoading, setFileLoading] = useState(false); const [fileName, setFileName] = useState(null); const fetchDir = useCallback(async (path: string): Promise => { const res = await fetch(`/api/projects/${projectId}/file?path=${encodeURIComponent(path)}`); const data = await res.json(); if (!res.ok) throw new Error(data.error); const items: FileItem[] = data.items ?? []; return items.filter(i => i.type !== "symlink") .sort((a, b) => a.type === "dir" && b.type !== "dir" ? -1 : a.type !== "dir" && b.type === "dir" ? 1 : a.name.localeCompare(b.name)) .map(i => ({ name: i.name, path: i.path, type: i.type === "dir" ? "dir" : "file", expanded: false, loaded: i.type !== "dir", children: i.type === "dir" ? [] : undefined })); }, [projectId]); useEffect(() => { if (!rootPath || status !== "authenticated") return; setTree([]); setSelectedPath(null); setFileContent(null); setTreeLoading(true); fetchDir(rootPath).then(nodes => { setTree(nodes); setTreeLoading(false); }).catch(() => setTreeLoading(false)); }, [rootPath, status, fetchDir]); const handleToggle = useCallback(async (path: string) => { setTree(prev => { const toggle = (nodes: TreeNode[]): TreeNode[] => nodes.map(n => n.path === path ? { ...n, expanded: !n.expanded } : n.children ? { ...n, children: toggle(n.children) } : n); return toggle(prev); }); const findNode = (nodes: TreeNode[], p: string): TreeNode | null => { for (const n of nodes) { if (n.path === p) return n; if (n.children) { const f = findNode(n.children, p); if (f) return f; } } return null; }; const node = findNode(tree, path); if (node && !node.loaded) { const children = await fetchDir(path).catch(() => []); setTree(prev => { const update = (nodes: TreeNode[]): TreeNode[] => nodes.map(n => n.path === path ? { ...n, children, loaded: true } : n.children ? { ...n, children: update(n.children) } : n); return update(prev); }); } }, [tree, fetchDir]); const handleSelectFile = useCallback(async (path: string) => { setSelectedPath(path); setFileContent(null); setFileName(path.split("/").pop() ?? null); setFileLoading(true); try { const res = await fetch(`/api/projects/${projectId}/file?path=${encodeURIComponent(path)}`); const data = await res.json(); setFileContent(data.content ?? ""); } catch { setFileContent("// Failed to load"); } finally { setFileLoading(false); } }, [projectId]); const lang = fileName ? langFromName(fileName) : "text"; const lines = (fileContent ?? "").split("\n"); if (!appName) { return (
Select an app
Choose an app from the left to browse its source files.
); } return (
{/* File tree */}
{appName}
{treeLoading &&
Loading…
} {!treeLoading && tree.length === 0 &&
Empty.
} {tree.map(n => )}
{/* Code viewer */}
{selectedPath ? ( {(() => { const rel = selectedPath.startsWith(rootPath + "/") ? selectedPath.slice(rootPath.length + 1) : selectedPath; return rel.split("/").map((s, i, a) => {i > 0 && /}{s}); })()} ) : Select a file} {fileName && {lang}}
{!selectedPath && !fileLoading &&
Select a file to view
} {fileLoading &&
Loading…
} {!fileLoading && fileContent !== null && (
{lines.map((_, i) =>
{i + 1}
)}
{highlightCode(fileContent, lang)}
)}
); } // ── Main Build hub ──────────────────────────────────────────────────────────── function BuildHubInner() { const params = useParams(); const searchParams = useSearchParams(); const router = useRouter(); const projectId = params.projectId as string; const workspace = params.workspace as string; const section = searchParams.get("section") ?? "code"; const activeApp = searchParams.get("app") ?? ""; const activeRoot = searchParams.get("root") ?? ""; const activeInfra = searchParams.get("tab") ?? "builds"; const activeSurfaceParam = searchParams.get("surface") ?? ""; const [apps, setApps] = useState([]); const [surfaces, setSurfaces] = useState([]); const [activeSurfaceId, setActiveSurfaceId] = useState(activeSurfaceParam); useEffect(() => { fetch(`/api/projects/${projectId}/apps`).then(r => r.json()).then(d => setApps(d.apps ?? [])).catch(() => {}); fetch(`/api/projects/${projectId}/design-surfaces`).then(r => r.json()).then(d => { const ids: string[] = d.surfaces ?? []; const themes: Record = d.surfaceThemes ?? {}; setSurfaces(ids.map(id => ({ id, label: SURFACE_LABELS[id] ?? id, lockedTheme: themes[id] }))); if (!activeSurfaceId && ids.length > 0) setActiveSurfaceId(ids[0]); }).catch(() => {}); }, [projectId]); const navigate = (params: Record) => { const sp = new URLSearchParams({ section, ...params }); router.push(`/${workspace}/project/${projectId}/build?${sp.toString()}`, { scroll: false }); }; const setSection = (s: string) => router.push(`/${workspace}/project/${projectId}/build?section=${s}`, { scroll: false }); return (
{/* ── Left nav ── */}
{/* Code group */}
Code
{apps.length > 0 ? apps.map(app => ( navigate({ section: "code", app: app.name, root: app.path })} /> )) : ( setSection("code")} /> )} {/* Layouts group */}
Layouts
{surfaces.length > 0 ? surfaces.map(s => ( { setActiveSurfaceId(s.id); navigate({ section: "layouts", surface: s.id }); }} /> )) : ( setSection("layouts")} /> )} {/* Infrastructure group */}
Infrastructure
{INFRA_ITEMS.map(item => ( navigate({ section: "infrastructure", tab: item.id })} /> ))}
{/* ── Content ── */}
{section === "code" && ( )} {section === "layouts" && ( { setActiveSurfaceId(id); navigate({ section: "layouts", surface: id }); }} /> )} {section === "infrastructure" && ( )}
); } export default function BuildPage() { return ( Loading…}> ); }