"use client"; import { useState, useEffect, useCallback } from "react"; import type { ReactElement, SyntheticEvent } from "react"; import { useRouter } from "next/navigation"; import { Plus, Trash2 } from "lucide-react"; import { MosaicSpinner } from "@/components/ui/MosaicSpinner"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { fetchProjects, createProject, deleteProject, ProjectStatus } from "@/lib/api/projects"; import type { Project, CreateProjectDto } from "@/lib/api/projects"; import { useWorkspaceId } from "@/lib/hooks"; import { fetchDomains } from "@/lib/api/domains"; import type { Domain } from "@mosaic/shared"; /* --------------------------------------------------------------------------- Status badge helpers --------------------------------------------------------------------------- */ interface StatusStyle { label: string; bg: string; color: string; } function getStatusStyle(status: ProjectStatus): StatusStyle { switch (status) { case ProjectStatus.PLANNING: return { label: "Planning", bg: "rgba(47,128,255,0.15)", color: "var(--primary)" }; case ProjectStatus.ACTIVE: return { label: "Active", bg: "rgba(20,184,166,0.15)", color: "var(--success)" }; case ProjectStatus.PAUSED: return { label: "Paused", bg: "rgba(245,158,11,0.15)", color: "var(--warn)" }; case ProjectStatus.COMPLETED: return { label: "Completed", bg: "rgba(139,92,246,0.15)", color: "var(--purple)" }; case ProjectStatus.ARCHIVED: return { label: "Archived", bg: "rgba(143,157,183,0.15)", color: "var(--muted)" }; default: return { label: String(status), bg: "rgba(143,157,183,0.15)", color: "var(--muted)" }; } } function formatTimestamp(iso: string): string { try { return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }); } catch { return iso; } } /* --------------------------------------------------------------------------- ProjectCard --------------------------------------------------------------------------- */ interface ProjectCardProps { project: Project; onDelete: (id: string) => void; onClick: (id: string) => void; domains: Domain[]; } function ProjectCard({ project, onDelete, onClick, domains }: ProjectCardProps): ReactElement { const [hovered, setHovered] = useState(false); const status = getStatusStyle(project.status); // Find domain if project has a domainId const domain = project.domainId ? domains.find((d) => d.id === project.domainId) : undefined; return (
{ onClick(project.id); }} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(project.id); } }} onMouseEnter={() => { setHovered(true); }} onMouseLeave={() => { setHovered(false); }} style={{ background: "var(--surface)", border: `1px solid ${hovered ? "var(--primary)" : "var(--border)"}`, borderRadius: "var(--r-lg)", padding: 20, cursor: "pointer", transition: "border-color 0.2s var(--ease)", display: "flex", flexDirection: "column", gap: 12, position: "relative", }} > {/* Header row: name + delete button */}

{project.name}

{/* Delete button */}
{/* Description */} {project.description ? (

{project.description}

) : (

No description

)} {/* Footer: status + timestamps */}
{/* Status badge */} {status.label} {domain && ( {domain.name} )} {/* Timestamps */} {formatTimestamp(project.createdAt)}
); } /* --------------------------------------------------------------------------- Create Project Dialog --------------------------------------------------------------------------- */ interface CreateDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSubmit: (data: CreateProjectDto) => Promise; isSubmitting: boolean; domains: Domain[]; } function CreateProjectDialog({ open, onOpenChange, onSubmit, isSubmitting, domains, }: CreateDialogProps): ReactElement { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [formError, setFormError] = useState(null); const [domainId, setDomainId] = useState(""); function resetForm(): void { setName(""); setDescription(""); setFormError(null); setDomainId(""); } async function handleSubmit(e: SyntheticEvent): Promise { e.preventDefault(); setFormError(null); setDomainId(""); const trimmedName = name.trim(); if (!trimmedName) { setFormError("Project name is required."); return; } try { const payload: CreateProjectDto = { name: trimmedName }; const trimmedDesc = description.trim(); if (trimmedDesc) { payload.description = trimmedDesc; } if (domainId) { payload.domainId = domainId; } await onSubmit(payload); resetForm(); } catch (err: unknown) { setFormError(err instanceof Error ? err.message : "Failed to create project."); } } return ( { if (!isOpen) resetForm(); onOpenChange(isOpen); }} >
New Project Give your project a name and optional description.
{ void handleSubmit(e); }} style={{ marginTop: 16 }} > {/* Name */}
{ setName(e.target.value); }} placeholder="e.g. Website Redesign" maxLength={255} autoFocus style={{ width: "100%", padding: "8px 12px", background: "var(--bg)", border: "1px solid var(--border)", borderRadius: "var(--r)", color: "var(--text)", fontSize: "0.9rem", outline: "none", boxSizing: "border-box", }} />
{/* Description */}