diff --git a/app/[workspace]/projects/page.tsx b/app/[workspace]/projects/page.tsx index cf4bbc40..9a606515 100644 --- a/app/[workspace]/projects/page.tsx +++ b/app/[workspace]/projects/page.tsx @@ -17,6 +17,7 @@ import { } from "@/components/ui/alert-dialog"; import { Loader2, Trash2 } from "lucide-react"; import { toast } from "sonner"; +import AgencyDashboard from "../agency/page"; interface ProjectWithStats { id: string; @@ -43,24 +44,64 @@ function timeAgo(dateStr?: string | null): 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"; + const color = + status === "live" + ? "#2e7d32" + : status === "building" + ? "#3d5afe" + : "#d4a04a"; + const anim = + status === "building" ? "vibn-breathe 2.5s ease infinite" : "none"; return ( - + ); } 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"; + 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} ); @@ -72,10 +113,12 @@ export default function ProjectsPage() { const { data: session, status } = useSession(); const [projects, setProjects] = useState([]); + const [isAgency, setIsAgency] = useState(null); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [showNew, setShowNew] = useState(false); - const [projectToDelete, setProjectToDelete] = useState(null); + const [projectToDelete, setProjectToDelete] = + useState(null); const [isDeleting, setIsDeleting] = useState(false); const [hoveredId, setHoveredId] = useState(null); @@ -83,19 +126,41 @@ export default function ProjectsPage() { try { setLoading(true); setLoadError(null); + + // Fetch Workspace metadata first to determine routing + const wsRes = await fetch(`/api/workspaces/${workspace}`, { + credentials: "include", + }); + if (wsRes.ok) { + const wsData = await wsRes.json(); + setIsAgency(wsData.isAgency); + if (wsData.isAgency) { + setLoading(false); + return; // Stop fetching projects if it's an agency, let AgencyDashboard handle its own data + } + } + const res = await fetch("/api/projects", { credentials: "include" }); if (res.status === 401) { throw new Error("Your session expired — please log in again."); } if (!res.ok) { let body: { error?: string; details?: string } = {}; - try { body = await res.json(); } catch { /* keep {} */ } - throw new Error(body.error || `HTTP ${res.status} ${res.statusText}`.trim()); + try { + body = await res.json(); + } catch { + /* keep {} */ + } + throw new Error( + body.error || `HTTP ${res.status} ${res.statusText}`.trim(), + ); } const data = await res.json(); setProjects(data.projects ?? []); } catch (err) { - setLoadError(err instanceof Error ? err.message : "Failed to load projects"); + setLoadError( + err instanceof Error ? err.message : "Failed to load projects", + ); } finally { setLoading(false); } @@ -133,7 +198,9 @@ export default function ProjectsPage() { const statusSummary = () => { const live = projects.filter((p) => p.status === "live").length; const building = projects.filter((p) => p.status === "building").length; - const defining = projects.filter((p) => !p.status || p.status === "defining").length; + const defining = projects.filter( + (p) => !p.status || p.status === "defining", + ).length; const parts = []; if (defining) parts.push(`${defining} defining`); if (building) parts.push(`${building} building`); @@ -141,63 +208,112 @@ export default function ProjectsPage() { return `${projects.length} total · ${parts.join(" · ")}`; }; + if (isAgency === true) { + return ; + } + return ( {/* Header */} - + - + Projects {!loading && ( - {statusSummary()} + + {statusSummary()} + )} setShowNew(true)} style={{ - display: "flex", alignItems: "center", gap: 6, - padding: "8px 16px", borderRadius: 7, - background: "#1a1a1a", color: "#fff", + display: "flex", + alignItems: "center", + gap: 6, + padding: "8px 16px", + borderRadius: 7, + background: "#1a1a1a", + color: "#fff", border: "1px solid #1a1a1a", - fontSize: "0.78rem", fontWeight: 600, - fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", cursor: "pointer", + fontSize: "0.78rem", + fontWeight: 600, + fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", + cursor: "pointer", }} > - + + + + + New project {/* Loading */} {loading && ( - - + + )} {/* Error */} {!loading && loadError && ( - + {loadError} @@ -218,11 +334,17 @@ export default function ProjectsPage() { { @@ -237,64 +359,137 @@ export default function ProjectsPage() { }} > {/* Project initial */} - - + + {p.productName[0]?.toUpperCase() ?? "P"} {/* Name + vision */} - - + + {p.productName} {p.productVision && ( - + {p.productVision} )} {/* Meta */} - + - + Last active - {timeAgo(p.updatedAt)} + + {timeAgo(p.updatedAt)} + - + Sessions - {p.stats.sessions} + + {p.stats.sessions} + {/* Delete (visible on row hover) */} { e.preventDefault(); e.stopPropagation(); setProjectToDelete(p); }} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setProjectToDelete(p); + }} style={{ - marginLeft: 16, padding: "6px 8px", borderRadius: 6, - border: "none", background: "transparent", - color: "#c0bab2", cursor: "pointer", + marginLeft: 16, + padding: "6px 8px", + borderRadius: 6, + border: "none", + background: "transparent", + color: "#c0bab2", + cursor: "pointer", opacity: hoveredId === p.id ? 1 : 0, transition: "opacity 0.15s, color 0.15s", - fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", flexShrink: 0, + fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", + flexShrink: 0, + }} + onMouseEnter={(e) => { + e.currentTarget.style.color = "#d32f2f"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.color = "#c0bab2"; }} - onMouseEnter={(e) => { e.currentTarget.style.color = "#d32f2f"; }} - onMouseLeave={(e) => { e.currentTarget.style.color = "#c0bab2"; }} title="Delete project" > @@ -307,17 +502,31 @@ export default function ProjectsPage() { setShowNew(true)} style={{ - width: "100%", display: "flex", alignItems: "center", justifyContent: "center", - padding: "22px", borderRadius: 10, - background: "transparent", border: "1px dashed #d0ccc4", - cursor: "pointer", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", - color: "#b5b0a6", fontSize: "0.84rem", fontWeight: 500, + width: "100%", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "22px", + borderRadius: 10, + background: "transparent", + border: "1px dashed #d0ccc4", + cursor: "pointer", + fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", + color: "#b5b0a6", + fontSize: "0.84rem", + fontWeight: 500, transition: "all 0.15s", animationDelay: `${projects.length * 0.05}s`, }} className="vibn-enter" - onMouseEnter={(e) => { e.currentTarget.style.borderColor = "#8a8478"; e.currentTarget.style.color = "#6b6560"; }} - onMouseLeave={(e) => { e.currentTarget.style.borderColor = "#d0ccc4"; e.currentTarget.style.color = "#b5b0a6"; }} + onMouseEnter={(e) => { + e.currentTarget.style.borderColor = "#8a8478"; + e.currentTarget.style.color = "#6b6560"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = "#d0ccc4"; + e.currentTarget.style.color = "#b5b0a6"; + }} > + New project @@ -327,19 +536,39 @@ export default function ProjectsPage() { {/* Empty state */} {!loading && projects.length === 0 && ( - + No projects yet - + Tell Vibn what you want to build and it will figure out the rest. setShowNew(true)} style={{ - padding: "10px 22px", borderRadius: 7, - background: "#1a1a1a", color: "#fff", - border: "none", fontSize: "0.84rem", fontWeight: 600, - fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", cursor: "pointer", + padding: "10px 22px", + borderRadius: 7, + background: "#1a1a1a", + color: "#fff", + border: "none", + fontSize: "0.84rem", + fontWeight: 600, + fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", + cursor: "pointer", }} > Create your first project @@ -349,17 +578,25 @@ export default function ProjectsPage() { { setShowNew(open); if (!open) fetchProjects(); }} + onOpenChange={(open) => { + setShowNew(open); + if (!open) fetchProjects(); + }} workspace={workspace} /> - !open && setProjectToDelete(null)}> + !open && setProjectToDelete(null)} + > - Delete "{projectToDelete?.productName}"? + + Delete "{projectToDelete?.productName}"? + - This will remove the project record. Sessions will be preserved but unlinked. - The Gitea repo will not be deleted automatically. + This will remove the project record. Sessions will be preserved + but unlinked. The Gitea repo will not be deleted automatically. @@ -369,7 +606,11 @@ export default function ProjectsPage() { disabled={isDeleting} className="bg-red-600 hover:bg-red-700" > - {isDeleting ? : } + {isDeleting ? ( + + ) : ( + + )} Delete Project
{statusSummary()}
+ {statusSummary()} +
+
Tell Vibn what you want to build and it will figure out the rest.