"use client"; import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { Loader2, AlertCircle, ChevronDown, ChevronRight, Box, Container, CircleDot, } from "lucide-react"; import { GiteaFileTree } from "@/components/project/gitea-file-tree"; import { GiteaFileViewer } from "@/components/project/gitea-file-viewer"; import { THEME, PageHeader, Card } from "@/components/project/dashboard-ui"; import { useAnatomy, type Anatomy } from "@/components/project/use-anatomy"; /** * Product tab — everything that makes up the thing being shipped. * * Left rail (top → bottom): * 1. Codebases — Gitea repos, each tile expands inline into a file * tree; clicking a file previews it on the right. * 2. Images — Coolify services backed by an upstream Docker image * (Twenty CRM, n8n…). Clicking shows image meta on the right. * * Dev containers do not appear here — they are the AI's workshop, not * part of the product surface. */ type Selection = { type: "file"; codebaseId: string; path: string } | null; export default function CodeTab() { const params = useParams(); const projectId = params.projectId as string; const { anatomy, loading, error } = useAnatomy(projectId); const codebases = anatomy?.product.codebases ?? null; const reason = anatomy?.codebasesReason; const [selection, setSelection] = useState(null); useEffect(() => { setSelection(null); }, [projectId]); const showLoading = loading && !anatomy; return (
{/* ── Left rail ── */}
{showLoading && (
Loading…
)} {error && !showLoading && (
{error}
)} {anatomy && ( <> {/* Code Files */} {codebases && codebases.length === 0 && ( {reason === "no_repo" ? ( <> No codebase yet.{" "} Try: "Start building my app" ) : ( <> Repo is empty — push a first commit.{" "} Try: "Scaffold a Next.js app" )} )} {codebases?.map((cb) => { return (
{codebases.length > 1 && (
{cb.label}
{cb.hint &&
{cb.hint}
}
)}
1 ? "0 0 16px 0" : "0 0 16px 0", }} > setSelection({ type: "file", codebaseId: cb.id, path: p, }) } />
); })}
)}
{/* ── Right pane ── */}
); } // ────────────────────────────────────────────────── // Bits // ────────────────────────────────────────────────── function RailGroup({ title, count, children, }: { title: string; count: number; children: React.ReactNode; }) { return (
{title} {count}
{children}
); } function RailEmpty({ children }: { children: React.ReactNode }) { return
{children}
; } function DetailRow({ label, value, dot, href, }: { label: string; value: string; dot?: string; href?: string; }) { return (
{label} {dot && } {href ? ( {value} ) : ( value )}
); } function Inline({ children }: { children: React.ReactNode }) { return (
{children}
); } function Empty({ children }: { children: React.ReactNode }) { return (
{children}
); } // ────────────────────────────────────────────────── function paneHeading(s: Selection): string { if (!s) return "Preview"; if (s.type === "file") return `Preview · ${shortPath(s.path)}`; return "Preview"; } function shortPath(p: string) { const parts = p.split("/"); if (parts.length <= 2) return p; return ".../" + parts.slice(-2).join("/"); } function statusColor(status: string) { const s = status.toLowerCase(); if (s.includes("running") || s.includes("healthy")) return "#2e7d32"; if (s.includes("starting") || s.includes("deploying")) return "#d4a04a"; if (s.includes("exit") || s.includes("fail") || s.includes("unhealthy")) return "#c5392b"; return "#a09a90"; } // ────────────────────────────────────────────────── // Tokens // ────────────────────────────────────────────────── const pageWrap: React.CSSProperties = { padding: "28px 48px 48px", fontFamily: THEME.font, color: THEME.ink, }; const grid: React.CSSProperties = { display: "grid", gridTemplateColumns: "300px minmax(0, 1fr)", alignItems: "stretch", flex: 1, minHeight: 0, }; const leftCol: React.CSSProperties = { minWidth: 0, display: "flex", flexDirection: "column", gap: 24, borderRight: `1px solid ${THEME.borderSoft}`, padding: "20px", overflowY: "auto", }; const rightCol: React.CSSProperties = { minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column", }; const heading: React.CSSProperties = { fontSize: "0.9rem", fontWeight: 600, color: THEME.ink, margin: "0 0 14px", }; const railGroup: React.CSSProperties = { display: "flex", flexDirection: "column", }; const railGroupHeader: React.CSSProperties = { display: "flex", alignItems: "center", justifyContent: "space-between", paddingBottom: 12, }; const railGroupTitle: React.CSSProperties = { fontSize: "0.95rem", fontWeight: 600, color: THEME.ink, }; const countPill: React.CSSProperties = { fontSize: "0.75rem", fontWeight: 600, color: THEME.mid, padding: "2px 8px", borderRadius: 999, background: THEME.borderSoft, }; const railItems: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 10, }; const railEmpty: React.CSSProperties = { padding: "10px 12px", fontSize: "0.74rem", color: THEME.muted, border: `1px dashed ${THEME.borderSoft}`, borderRadius: 8, lineHeight: 1.6, }; const nudge: React.CSSProperties = { display: "block", marginTop: 6, fontStyle: "normal", background: "#f3eee4", borderRadius: 4, padding: "3px 8px", fontSize: "0.72rem", color: "#7a6a50", }; const flatTile: React.CSSProperties = { display: "flex", alignItems: "center", gap: 10, width: "100%", padding: "12px 14px", background: THEME.cardBg, border: `1px solid ${THEME.borderSoft}`, borderRadius: 10, cursor: "pointer", font: "inherit", color: "inherit", transition: "border-color 0.12s, background 0.12s, box-shadow 0.12s", }; const codebaseTile: React.CSSProperties = { background: THEME.cardBg, border: `1px solid ${THEME.borderSoft}`, borderRadius: 10, overflow: "hidden", }; const tileHeader: React.CSSProperties = { display: "flex", alignItems: "center", gap: 8, width: "100%", padding: "12px 14px", background: "transparent", border: "none", font: "inherit", color: "inherit", }; const tileLabel: React.CSSProperties = { fontSize: "0.85rem", fontWeight: 600, color: THEME.ink, marginBottom: 2, }; const tileHint: React.CSSProperties = { fontSize: "0.74rem", color: THEME.mid, lineHeight: 1.4, }; const tileBody: React.CSSProperties = { padding: "8px 10px 12px", borderTop: `1px solid ${THEME.borderSoft}`, }; const chevronCell: React.CSSProperties = { width: 14, display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0, }; const panel: React.CSSProperties = { background: THEME.cardBg, border: `1px solid ${THEME.border}`, borderRadius: 10, padding: 16, flex: 1, minHeight: 0, display: "flex", flexDirection: "column", }; const detailRow: React.CSSProperties = { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 4px", borderBottom: `1px solid ${THEME.borderSoft}`, }; const detailLabel: React.CSSProperties = { fontSize: "0.72rem", fontWeight: 600, letterSpacing: "0.06em", textTransform: "uppercase", color: THEME.muted, }; const detailValue: React.CSSProperties = { fontSize: "0.85rem", color: THEME.ink, display: "inline-flex", alignItems: "center", }; const detailLink: React.CSSProperties = { color: THEME.ink, textDecoration: "underline", };