diff --git a/components/layout/project-shell.tsx b/components/layout/project-shell.tsx index 100c3b2..8aa2a88 100644 --- a/components/layout/project-shell.tsx +++ b/components/layout/project-shell.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import { ReactNode, useEffect, useState } from "react"; +import { ReactNode } from "react"; import { VIBNSidebar } from "./vibn-sidebar"; import { Toaster } from "sonner"; @@ -23,14 +23,14 @@ interface ProjectShellProps { } const ALL_TABS = [ - { id: "overview", label: "Atlas", path: "overview" }, - { id: "prd", label: "PRD", path: "prd" }, - { id: "design", label: "Design", path: "design" }, - { id: "build", label: "Build", path: "build" }, - { id: "deployment", label: "Launch", path: "deployment" }, - { id: "grow", label: "Grow", path: "grow" }, - { id: "insights", label: "Insights", path: "insights" }, - { id: "settings", label: "Settings", path: "settings" }, + { id: "overview", label: "Atlas", path: "overview" }, + { id: "prd", label: "PRD", path: "prd" }, + { id: "design", label: "Design", path: "design" }, + { id: "build", label: "Build", path: "build" }, + { id: "deployment", label: "Launch", path: "deployment" }, + { id: "grow", label: "Grow", path: "grow" }, + { id: "insights", label: "Insights", path: "insights" }, + { id: "settings", label: "Settings", path: "settings" }, ]; function getTabsForMode( @@ -38,10 +38,8 @@ function getTabsForMode( ) { switch (mode) { case "code-import": - // Hide PRD — this project already has code; goal is go-to-market surfaces return ALL_TABS.filter(t => t.id !== "prd"); case "migration": - // Hide PRD, rename overview, hide Grow and Insights (less relevant) return ALL_TABS .filter(t => !["prd", "grow", "insights"].includes(t.id)) .map(t => t.id === "overview" ? { ...t, label: "Migration Plan" } : t); @@ -50,323 +48,79 @@ function getTabsForMode( } } -const DISCOVERY_PHASES = [ - { id: "big_picture", label: "Big Picture" }, - { id: "users_personas", label: "Users & Personas" }, - { id: "features_scope", label: "Features" }, - { id: "business_model", label: "Business Model" }, - { id: "screens_data", label: "Screens" }, - { id: "risks_questions", label: "Risks" }, -]; - -interface SavedPhase { - phase: string; - title: string; - summary: string; - data: Record; - saved_at: string; -} - -function timeAgo(dateStr?: string): string { - if (!dateStr) return "—"; - const date = new Date(dateStr); - if (isNaN(date.getTime())) return "—"; - const diff = (Date.now() - date.getTime()) / 1000; - if (diff < 60) return "just now"; - if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; - if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; - const days = Math.floor(diff / 86400); - if (days === 1) return "Yesterday"; - if (days < 7) return `${days}d ago`; - return new Date(dateStr).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); -} - -function SectionLabel({ children }: { children: ReactNode }) { - return ( -
- {children} -
- ); -} - -function StatusTag({ status }: { status?: string }) { - const label = status === "live" ? "Live" : status === "building" ? "Building" : "Defining"; - const color = status === "live" ? "#2e7d32" : status === "building" ? "#3d5afe" : "#9a7b3a"; - const bg = status === "live" ? "#2e7d3210" : status === "building" ? "#3d5afe10" : "#d4a04a12"; - return ( - - {label} - - ); -} - export function ProjectShell({ children, workspace, projectId, - projectName, - projectDescription, - projectStatus, - projectProgress, - createdAt, - updatedAt, - featureCount = 0, creationMode, }: ProjectShellProps) { const pathname = usePathname(); const TABS = getTabsForMode(creationMode); - const activeTab = TABS.find((t) => pathname?.includes(`/${t.path}`))?.id ?? "overview"; - const progress = projectProgress ?? 0; - - const [savedPhases, setSavedPhases] = useState([]); - - useEffect(() => { - fetch(`/api/projects/${projectId}/save-phase`) - .then(r => r.json()) - .then(d => setSavedPhases(d.phases ?? [])) - .catch(() => {}); - - // Refresh every 10s while the user is chatting with Atlas - const interval = setInterval(() => { - fetch(`/api/projects/${projectId}/save-phase`) - .then(r => r.json()) - .then(d => setSavedPhases(d.phases ?? [])) - .catch(() => {}); - }, 10_000); - return () => clearInterval(interval); - }, [projectId]); - - const savedPhaseIds = new Set(savedPhases.map(p => p.phase)); - const firstUnsavedIdx = DISCOVERY_PHASES.findIndex(p => !savedPhaseIds.has(p.id)); + const activeTab = TABS.find(t => pathname?.includes(`/${t.path}`))?.id ?? "overview"; return ( <> +
+ {/* Left sidebar */}
- {/* Main column */} + {/* Main column — full width */}
- {/* Project header */} -
-
-
- - {projectName[0]?.toUpperCase() ?? "P"} - -
-
-
-

- {projectName} -

- -
- {projectDescription && ( -

- {projectDescription} -

- )} -
-
-
- {progress}% -
-
- - {/* Tab bar */} + {/* Tab bar — sits at the top, no header above it */}
- {TABS.map((t) => ( + {TABS.map(t => ( { if (activeTab !== t.id) (e.currentTarget as HTMLElement).style.color = "#6b6560"; }} + onMouseLeave={e => { if (activeTab !== t.id) (e.currentTarget as HTMLElement).style.color = "#a09a90"; }} > {t.label} ))}
- {/* Page content */} + {/* Page content — full width, each page manages its own layout */}
{children}
- - {/* Right panel — hidden on design tab (design page has its own right panel) */} -
- {/* Right panel content — varies by creation mode */} - {(creationMode === "code-import" || creationMode === "migration") ? ( - <> - - {creationMode === "migration" ? "Migration" : "Import"} - -
- {creationMode === "migration" - ? "Atlas will audit your existing product and generate a safe, phased migration plan." - : "Atlas will clone your repository and map the architecture, then suggest surfaces to build."} -
- - ) : ( - <> - {/* Discovery phases */} - Discovery - {DISCOVERY_PHASES.map((phase, i) => { - const isDone = savedPhaseIds.has(phase.id); - const isActive = !isDone && i === firstUnsavedIdx; - return ( -
-
- {isDone ? "✓" : isActive ? "→" : i + 1} -
- - {phase.label} - -
- ); - })} - -
- - {/* Captured data — summaries from saved phases */} - Captured - {savedPhases.length > 0 ? ( - savedPhases.map((p) => ( -
-
- {p.title} -
-
- {p.summary} -
-
- )) - ) : ( -

- Atlas will capture key details here as you chat. -

- )} - - )} - -
- - {/* Project info — always shown */} - Project Info - {[ - { k: "Created", v: timeAgo(createdAt) }, - { k: "Last active", v: timeAgo(updatedAt) }, - { k: "Features", v: featureCount > 0 ? `${featureCount} defined` : "None yet" }, - ].map((item, i) => ( -
-
- {item.k} -
-
{item.v}
-
- ))} -
+ ); diff --git a/components/layout/vibn-sidebar.tsx b/components/layout/vibn-sidebar.tsx index f3d74cf..357901e 100644 --- a/components/layout/vibn-sidebar.tsx +++ b/components/layout/vibn-sidebar.tsx @@ -5,43 +5,138 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { signOut, useSession } from "next-auth/react"; -interface Project { - id: string; - productName: string; - status?: string; -} - interface VIBNSidebarProps { workspace: string; } -function StatusDot({ status }: { status?: string }) { - const color = - status === "live" ? "#2e7d32" - : status === "building" ? "#3d5afe" - : "#d4a04a"; - const anim = status === "building" ? "vibn-breathe 2.5s ease infinite" : "none"; +interface ProjectData { + id: string; + productName?: string; + name?: string; + status?: string; + giteaRepo?: string; + giteaRepoUrl?: string; + surfaces?: string[]; + surfaceThemes?: Record; + apps?: Array<{ name: string; path: string; coolifyServiceUuid?: string | null; domain?: string | null }>; +} + +interface AppEntry { + name: string; + path: string; +} + +// ── Section helpers ───────────────────────────────────────────────────────── + +function SectionHeading({ label, collapsed }: { label: string; collapsed: boolean }) { + if (collapsed) return null; return ( - +
+ {label} +
); } +function SectionRow({ + icon, label, href, dim, collapsed, +}: { + icon: string; + label: string; + href?: string; + dim?: boolean; + collapsed: boolean; +}) { + const style: React.CSSProperties = { + display: "flex", alignItems: "center", + justifyContent: collapsed ? "center" : "flex-start", + gap: 8, padding: collapsed ? "7px 0" : "5px 12px", + borderRadius: 5, textDecoration: "none", + color: dim ? "#c5c0b8" : "#4a4640", + fontSize: "0.78rem", fontWeight: 450, + transition: "background 0.1s", + width: "100%", boxSizing: "border-box" as const, + }; + + const inner = ( + <> + + {icon} + + {!collapsed && ( + + {label} + + )} + + ); + + if (href) { + return ( + { if (!dim) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }} + onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = "transparent"; }} + > + {inner} + + ); + } + return ( +
+ {inner} +
+ ); +} + +function SectionDivider() { + return
; +} + +// ── Surface label map ──────────────────────────────────────────────────────── +const SURFACE_LABELS: Record = { + "marketing": "Marketing site", + "web-app": "Web app", + "admin": "Admin panel", + "api": "API layer", +}; + +const SURFACE_ICONS: Record = { + "marketing": "◎", + "web-app": "⬡", + "admin": "◫", + "api": "⌁", +}; + +// ── Main sidebar ───────────────────────────────────────────────────────────── + const COLLAPSED_KEY = "vibn_sidebar_collapsed"; -const COLLAPSED_W = 56; -const EXPANDED_W = 220; +const COLLAPSED_W = 52; +const EXPANDED_W = 216; export function VIBNSidebar({ workspace }: VIBNSidebarProps) { const pathname = usePathname(); const { data: session } = useSession(); - const [projects, setProjects] = useState([]); + const [collapsed, setCollapsed] = useState(false); const [mounted, setMounted] = useState(false); - // Restore collapse state from localStorage + // Project-specific data + const [project, setProject] = useState(null); + const [apps, setApps] = useState([]); + + // Global projects list (used when NOT inside a project) + const [projects, setProjects] = useState>([]); + + const activeProjectId = pathname?.match(/\/project\/([^/]+)/)?.[1] ?? null; + const activeTab = pathname?.match(/\/project\/[^/]+\/([^/]+)/)?.[1] ?? null; + + // Restore collapse state useEffect(() => { const stored = localStorage.getItem(COLLAPSED_KEY); if (stored === "1") setCollapsed(true); @@ -55,14 +150,30 @@ export function VIBNSidebar({ workspace }: VIBNSidebarProps) { }); }; + // Fetch global projects list (for non-project pages) useEffect(() => { + if (activeProjectId) return; fetch("/api/projects") - .then((r) => r.json()) - .then((d) => setProjects(d.projects ?? [])) + .then(r => r.json()) + .then(d => setProjects(d.projects ?? [])) .catch(() => {}); - }, []); + }, [activeProjectId]); + + // Fetch project-specific data when inside a project + useEffect(() => { + if (!activeProjectId) { setProject(null); setApps([]); return; } + + fetch(`/api/projects/${activeProjectId}`) + .then(r => r.json()) + .then(d => setProject(d.project ?? null)) + .catch(() => {}); + + fetch(`/api/projects/${activeProjectId}/apps`) + .then(r => r.json()) + .then(d => setApps(d.apps ?? [])) + .catch(() => {}); + }, [activeProjectId]); - const activeProjectId = pathname?.match(/\/project\/([^/]+)/)?.[1] ?? null; const isProjects = !activeProjectId && (pathname?.includes("/projects") || pathname?.includes("/project")); const isActivity = !activeProjectId && pathname?.includes("/activity"); const isSettings = !activeProjectId && pathname?.includes("/settings"); @@ -78,117 +189,90 @@ export function VIBNSidebar({ workspace }: VIBNSidebarProps) { ?? "?"; const w = collapsed ? COLLAPSED_W : EXPANDED_W; - - // Don't animate on initial mount (avoid flash) const transition = mounted ? "width 0.2s cubic-bezier(0.4,0,0.2,1)" : "none"; + const base = `/${workspace}/project/${activeProjectId}`; + + // Surfaces locked in on design page + const surfaces = project?.surfaces ?? []; + // Coolify/monorepo apps + const infraApps = project?.apps ?? []; + return (