"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useSession } from "next-auth/react"; import { Plus_Jakarta_Sans } from "next/font/google"; import { Loader2, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { ProjectCreationModal } from "@/components/project-creation-modal"; import { isClientDevProjectBypass } from "@/lib/dev-bypass"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; const justineJakarta = Plus_Jakarta_Sans({ subsets: ["latin"], weight: ["400", "500", "600", "700", "800"], variable: "--font-justine-jakarta", display: "swap", }); interface ProjectWithStats { id: string; productName: string; productVision?: string; status?: string; updatedAt: string | null; stats: { sessions: number; costs: number }; } const ICON_BG = ["#6366F1", "#8B5CF6", "#06B6D4", "#EC4899", "#9CA3AF"]; 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 greetingName(session: { user?: { name?: string | null; email?: string | null } } | null): string { const n = session?.user?.name?.trim(); if (n) return n.split(/\s+/)[0] ?? "there"; const e = session?.user?.email; if (e) return e.split("@")[0] ?? "there"; return "there"; } function greetingPrefix(): string { const h = new Date().getHours(); if (h < 12) return "Good morning"; if (h < 17) return "Good afternoon"; return "Good evening"; } function StatusPill({ status }: { status?: string }) { if (status === "live") { return ( Live ); } if (status === "building") { return ( Building ); } return Defining; } export function JustineWorkspaceProjectsDashboard({ workspace }: { workspace: string }) { const { data: session, status: authStatus } = useSession(); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(""); const [showNew, setShowNew] = useState(false); const [projectToDelete, setProjectToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const [theme, setTheme] = useState<"light" | "dark">("light"); useEffect(() => { try { const t = localStorage.getItem("jd-dashboard-theme"); if (t === "dark") setTheme("dark"); } catch { /* ignore */ } }, []); const toggleTheme = useCallback(() => { setTheme((prev) => { const next = prev === "light" ? "dark" : "light"; try { localStorage.setItem("jd-dashboard-theme", next); } catch { /* ignore */ } return next; }); }, []); const fetchProjects = useCallback(async () => { try { setLoading(true); const res = await fetch("/api/projects"); if (!res.ok) throw new Error("Failed"); const data = await res.json(); setProjects(data.projects ?? []); } catch { /* silent */ } finally { setLoading(false); } }, []); useEffect(() => { if (isClientDevProjectBypass()) { void fetchProjects(); return; } if (authStatus === "authenticated") fetchProjects(); else if (authStatus === "unauthenticated") setLoading(false); }, [authStatus, fetchProjects]); const filtered = useMemo(() => { const q = search.trim().toLowerCase(); if (!q) return projects; return projects.filter((p) => p.productName.toLowerCase().includes(q)); }, [projects, search]); const liveN = projects.filter((p) => p.status === "live").length; const buildingN = projects.filter((p) => p.status === "building").length; const totalCosts = projects.reduce((s, p) => s + (p.stats?.costs ?? 0), 0); const userInitial = session?.user?.name?.[0]?.toUpperCase() ?? session?.user?.email?.[0]?.toUpperCase() ?? "?"; const displayName = session?.user?.name?.trim() || session?.user?.email?.split("@")[0] || "Account"; 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"); } } catch { toast.error("An error occurred"); } finally { setIsDeleting(false); } }; const firstName = greetingName(session); return (

{greetingPrefix()}, {firstName}.

Open a project from the sidebar or start a new one.

{!loading && projects.length === 0 && (

Build your first product

Describe your idea, and vibn will architect, design, and help you ship it — no code required.

)}
Portfolio snapshot
{projects.length}
Active projects
{liveN}
Live products
{buildingN}
Building now
{totalCosts > 0 ? `$${totalCosts.toFixed(2)}` : "—"}
API spend (est.)
{ 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
); }