"use client"; import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; import { useParams } from "next/navigation"; import Link from "next/link"; import { ProjectCreationModal } from "@/components/project-creation-modal"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Loader2, Trash2 } from "lucide-react"; import { toast } from "sonner"; interface ProjectWithStats { id: string; productName: string; productVision?: string; status?: string; updatedAt: string | null; stats: { sessions: number; costs: number }; } function timeAgo(dateStr?: string | null): 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`; if (days < 30) return `${Math.floor(days / 7)}w ago`; return `${Math.floor(days / 30)}mo ago`; } 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"; 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"; return ( {label} ); } export default function ProjectsPage() { const params = useParams(); const workspace = params.workspace as string; const { data: session, status } = useSession(); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [showNew, setShowNew] = useState(false); const [projectToDelete, setProjectToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const [hoveredId, setHoveredId] = useState(null); const fetchProjects = async () => { try { setLoading(true); const res = await fetch("/api/projects"); if (!res.ok) throw new Error("Failed to fetch projects"); const data = await res.json(); setProjects(data.projects ?? []); } catch { /* silent */ } finally { setLoading(false); } }; useEffect(() => { if (status === "authenticated") fetchProjects(); else if (status === "unauthenticated") setLoading(false); }, [status]); const handleDelete = async () => { if (!projectToDelete) return; setIsDeleting(true); try { const res = await fetch("/api/projects/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ projectId: projectToDelete.id }), }); if (res.ok) { toast.success("Project deleted"); setProjectToDelete(null); fetchProjects(); } else { const err = await res.json(); toast.error(err.error || "Failed to delete project"); } } catch { toast.error("An error occurred"); } finally { setIsDeleting(false); } }; 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 parts = []; if (defining) parts.push(`${defining} defining`); if (building) parts.push(`${building} building`); if (live) parts.push(`${live} live`); return `${projects.length} total · ${parts.join(" · ")}`; }; return (
{/* Header */}

Projects

{!loading && (

{statusSummary()}

)}
{/* Loading */} {loading && (
)} {/* Project list */} {!loading && (
{projects.map((p, i) => (
{ setHoveredId(p.id); e.currentTarget.style.borderColor = "#d0ccc4"; e.currentTarget.style.boxShadow = "0 2px 8px #1a1a1a0a"; }} onMouseLeave={(e) => { setHoveredId(null); e.currentTarget.style.borderColor = "#e8e4dc"; e.currentTarget.style.boxShadow = "0 1px 2px #1a1a1a05"; }} > {/* Project initial */}
{p.productName[0]?.toUpperCase() ?? "P"}
{/* Name + vision */}
{p.productName}
{p.productVision && ( {p.productVision} )}
{/* Meta */}
Last active
{timeAgo(p.updatedAt)}
Sessions
{p.stats.sessions}
{/* Delete (visible on row hover) */}
))} {/* New project card */}
)} {/* Empty state */} {!loading && projects.length === 0 && (

No projects yet

Tell Vibn what you want to build and it will figure out the rest.

)} { setShowNew(open); if (!open) fetchProjects(); }} workspace={workspace} /> !open && setProjectToDelete(null)}> Delete "{projectToDelete?.productName}"? This will remove the project record. Sessions will be preserved but unlinked. The Gitea repo will not be deleted automatically. Cancel {isDeleting ? : } Delete Project
); }