"use client"; import { useState, useRef, useCallback } from "react"; import { Plus, FolderKanban, ChevronRight, Maximize2, Minimize2, X as XIcon, UserMinus, Check } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { projectListOptions } from "@multica/core/projects/queries"; import { useCreateProject, useUpdateProject } from "@multica/core/projects/mutations"; import { PROJECT_STATUS_CONFIG, PROJECT_STATUS_ORDER, PROJECT_PRIORITY_CONFIG, PROJECT_PRIORITY_ORDER } from "@multica/core/projects/config"; import { useWorkspaceId } from "@multica/core/hooks"; import { useWorkspaceStore } from "@multica/core/workspace"; import { memberListOptions, agentListOptions } from "@multica/core/workspace/queries"; import { AppLink, useNavigation } from "../../navigation"; import { ActorAvatar } from "../../common/actor-avatar"; import { useActorName } from "@multica/core/workspace/hooks"; import { Skeleton } from "@multica/ui/components/ui/skeleton"; import { Button } from "@multica/ui/components/ui/button"; import { cn } from "@multica/ui/lib/utils"; import { toast } from "sonner"; import { Dialog, DialogContent, DialogTitle, } from "@multica/ui/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@multica/ui/components/ui/dropdown-menu"; import { Popover, PopoverTrigger, PopoverContent, } from "@multica/ui/components/ui/popover"; import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/ui/tooltip"; import { ContentEditor, type ContentEditorRef } from "../../editor"; import { TitleEditor } from "../../editor"; import { EmojiPicker } from "@multica/ui/components/common/emoji-picker"; import type { Project, ProjectStatus, ProjectPriority, UpdateProjectRequest } from "@multica/core/types"; import { PriorityIcon } from "../../issues/components/priority-icon"; function formatRelativeDate(date: string): string { const diff = Date.now() - new Date(date).getTime(); const days = Math.floor(diff / (1000 * 60 * 60 * 24)); if (days < 1) return "Today"; if (days === 1) return "1d ago"; if (days < 30) return `${days}d ago`; const months = Math.floor(days / 30); return `${months}mo ago`; } function ProjectRow({ project }: { project: Project }) { const wsId = useWorkspaceId(); const statusCfg = PROJECT_STATUS_CONFIG[project.status]; const priorityCfg = PROJECT_PRIORITY_CONFIG[project.priority]; const updateProject = useUpdateProject(); const { data: members = [] } = useQuery(memberListOptions(wsId)); const { data: agents = [] } = useQuery(agentListOptions(wsId)); const { getActorName } = useActorName(); const [leadOpen, setLeadOpen] = useState(false); const [leadFilter, setLeadFilter] = useState(""); const leadQuery = leadFilter.toLowerCase(); const filteredMembers = members.filter((m) => m.name.toLowerCase().includes(leadQuery)); const filteredAgents = agents.filter((a) => !a.archived_at && a.name.toLowerCase().includes(leadQuery)); const handleUpdate = useCallback( (data: UpdateProjectRequest) => { updateProject.mutate({ id: project.id, ...data }); }, [project.id, updateProject], ); return (
{/* Icon + Name (navigates to detail) */} {project.icon || "📁"} {project.title} {/* Priority — dropdown */} {priorityCfg.label} } /> {PROJECT_PRIORITY_ORDER.map((p) => ( handleUpdate({ priority: p as ProjectPriority })}> {PROJECT_PRIORITY_CONFIG[p].label} {p === project.priority && } ))} {/* Status — dropdown */} {statusCfg.label} } /> {PROJECT_STATUS_ORDER.map((s) => ( handleUpdate({ status: s as ProjectStatus })}> {PROJECT_STATUS_CONFIG[s].label} {s === project.status && } ))} {/* Progress (read-only) */} {project.issue_count > 0 ? ( <> {project.done_count}/{project.issue_count} ) : ( -- )} {/* Lead — popover */} { setLeadOpen(v); if (!v) setLeadFilter(""); }}> {project.lead_type && project.lead_id ? ( } /> {getActorName(project.lead_type, project.lead_id)} ) : ( )} } />
setLeadFilter(e.target.value)} placeholder="Assign lead..." className="w-full bg-transparent text-sm placeholder:text-muted-foreground outline-none" />
{filteredMembers.length > 0 && ( <>
Members
{filteredMembers.map((m) => ( ))} )} {filteredAgents.length > 0 && ( <>
Agents
{filteredAgents.map((a) => ( ))} )} {filteredMembers.length === 0 && filteredAgents.length === 0 && leadFilter && (
No results
)}
{/* Created */} {formatRelativeDate(project.created_at)}
); } function PillButton({ children, className, ...props }: React.ButtonHTMLAttributes) { return ( ); } function CreateProjectDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { const router = useNavigation(); const workspaceName = useWorkspaceStore((s) => s.workspace?.name); const wsId = useWorkspaceId(); const { data: members = [] } = useQuery(memberListOptions(wsId)); const { data: agents = [] } = useQuery(agentListOptions(wsId)); const { getActorName } = useActorName(); const [title, setTitle] = useState(""); const descEditorRef = useRef(null); const [status, setStatus] = useState("planned"); const [priority, setPriority] = useState("none"); const [leadType, setLeadType] = useState<"member" | "agent" | undefined>(); const [leadId, setLeadId] = useState(); const [icon, setIcon] = useState(); const [iconPickerOpen, setIconPickerOpen] = useState(false); const [submitting, setSubmitting] = useState(false); const [isExpanded, setIsExpanded] = useState(false); // Lead popover const [leadOpen, setLeadOpen] = useState(false); const [leadFilter, setLeadFilter] = useState(""); const leadQuery = leadFilter.toLowerCase(); const filteredMembers = members.filter((m) => m.name.toLowerCase().includes(leadQuery)); const filteredAgents = agents.filter((a) => !a.archived_at && a.name.toLowerCase().includes(leadQuery)); const leadLabel = leadType && leadId ? getActorName(leadType, leadId) : "Lead"; const createProject = useCreateProject(); const handleSubmit = async () => { if (!title.trim() || submitting) return; setSubmitting(true); try { const project = await createProject.mutateAsync({ title: title.trim(), description: descEditorRef.current?.getMarkdown()?.trim() || undefined, icon, status, priority, lead_type: leadType, lead_id: leadId, }); onOpenChange(false); setTitle(""); setIcon(undefined); setStatus("planned"); setPriority("none"); setLeadType(undefined); setLeadId(undefined); toast.success("Project created"); router.push(`/projects/${project.id}`); } catch { toast.error("Failed to create project"); } finally { setSubmitting(false); } }; return ( New Project {/* Header */}
{workspaceName} New project
setIsExpanded(!isExpanded)} className="rounded-sm p-1.5 opacity-70 hover:opacity-100 hover:bg-accent/60 transition-all cursor-pointer" > {isExpanded ? : } } /> {isExpanded ? "Collapse" : "Expand"} onOpenChange(false)} className="rounded-sm p-1.5 opacity-70 hover:opacity-100 hover:bg-accent/60 transition-all cursor-pointer" > } /> Close
{/* Icon + Title */}
{icon || "📁"} } /> { setIcon(emoji); setIconPickerOpen(false); }} /> setTitle(v)} onSubmit={handleSubmit} />
{/* Description */}
{/* Property toolbar */}
{/* Status */} {PROJECT_STATUS_CONFIG[status].label} } /> {PROJECT_STATUS_ORDER.map((s) => ( setStatus(s)}> {PROJECT_STATUS_CONFIG[s].label} ))} {/* Priority */} {PROJECT_PRIORITY_CONFIG[priority].label} } /> {PROJECT_PRIORITY_ORDER.map((p) => ( setPriority(p)}> {PROJECT_PRIORITY_CONFIG[p].label} ))} {/* Lead */} { setLeadOpen(v); if (!v) setLeadFilter(""); }}> {leadType && leadId ? ( <> {leadLabel} ) : ( Lead )} } />
setLeadFilter(e.target.value)} placeholder="Assign lead..." className="w-full bg-transparent text-sm placeholder:text-muted-foreground outline-none" />
{filteredMembers.length > 0 && ( <>
Members
{filteredMembers.map((m) => ( ))} )} {filteredAgents.length > 0 && ( <>
Agents
{filteredAgents.map((a) => ( ))} )} {filteredMembers.length === 0 && filteredAgents.length === 0 && leadFilter && (
No results
)}
{/* Footer */}
); } export function ProjectsPage() { const wsId = useWorkspaceId(); const { data: projects = [], isLoading } = useQuery(projectListOptions(wsId)); const [createOpen, setCreateOpen] = useState(false); return (
{/* Header bar */}

Projects

{!isLoading && projects.length > 0 && ( {projects.length} )}
{/* Table */}
{isLoading ? (
{Array.from({ length: 4 }).map((_, i) => ( ))}
) : projects.length === 0 ? (

No projects yet

) : ( <> {/* Column headers */}
{/* Icon spacer + Name */} Name Priority Status Progress Lead Created
{/* Rows */} {projects.map((project) => ( ))} )}
); }