import { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { Project } from "@paperclipai/shared"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; import { useDialogActions } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { EntityRow } from "../components/EntityRow"; import { StatusBadge } from "../components/StatusBadge"; import { MembershipAction } from "../components/MembershipAction"; import { EmptyState } from "../components/EmptyState"; import { PageSkeleton } from "../components/PageSkeleton"; import { formatDate, projectUrl } from "../lib/utils"; import { resourceMembershipState, useResourceMembershipMutation, useResourceMemberships, } from "../hooks/useResourceMemberships"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { ArrowUpDown, Check, Hexagon, Plus } from "lucide-react"; type ProjectSortField = "name" | "updated" | "created" | "targetDate"; type ProjectSortDir = "asc" | "desc"; const PROJECT_SORT_OPTIONS: Array<{ field: ProjectSortField; label: string }> = [ { field: "name", label: "Name" }, { field: "updated", label: "Updated" }, { field: "created", label: "Created" }, { field: "targetDate", label: "Target date" }, ]; function compareProjectNames(left: Project, right: Project) { const nameDiff = left.name.localeCompare(right.name, undefined, { sensitivity: "base" }); return nameDiff !== 0 ? nameDiff : left.id.localeCompare(right.id); } function projectTime(value: Date | string | null | undefined): number | null { if (!value) return null; const time = new Date(value).getTime(); return Number.isFinite(time) ? time : null; } function compareOptionalTime( left: Date | string | null | undefined, right: Date | string | null | undefined, sortDir: ProjectSortDir, ) { const leftTime = projectTime(left); const rightTime = projectTime(right); if (leftTime === null && rightTime === null) return 0; if (leftTime === null) return 1; if (rightTime === null) return -1; return sortDir === "asc" ? leftTime - rightTime : rightTime - leftTime; } function sortProjects(projects: Project[], sortField: ProjectSortField, sortDir: ProjectSortDir) { return [...projects].sort((left, right) => { let comparison = 0; if (sortField === "name") { comparison = compareProjectNames(left, right); return sortDir === "asc" ? comparison : -comparison; } if (sortField === "updated") comparison = compareOptionalTime(left.updatedAt, right.updatedAt, sortDir); else if (sortField === "created") comparison = compareOptionalTime(left.createdAt, right.createdAt, sortDir); else comparison = compareOptionalTime(left.targetDate, right.targetDate, sortDir); if (comparison === 0) comparison = compareProjectNames(left, right); return comparison; }); } export function Projects() { const { selectedCompanyId } = useCompany(); const { openNewProject } = useDialogActions(); const { setBreadcrumbs } = useBreadcrumbs(); const [sortField, setSortField] = useState("name"); const [sortDir, setSortDir] = useState("asc"); useEffect(() => { setBreadcrumbs([{ label: "Projects" }]); }, [setBreadcrumbs]); const { data: allProjects, isLoading, error } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const membershipsQuery = useResourceMemberships(selectedCompanyId); const membershipMutation = useResourceMembershipMutation(selectedCompanyId); const projects = useMemo( () => (allProjects ?? []).filter((p) => !p.archivedAt), [allProjects], ); const sortedProjects = useMemo( () => sortProjects(projects, sortField, sortDir), [projects, sortDir, sortField], ); const groupedProjects = useMemo(() => { const groups = { mine: [] as typeof sortedProjects, other: [] as typeof sortedProjects, }; for (const project of sortedProjects) { const state = resourceMembershipState(membershipsQuery.data, "project", project.id); if (state === "left") groups.other.push(project); else groups.mine.push(project); } return groups; }, [membershipsQuery.data, sortedProjects]); const sortLabel = PROJECT_SORT_OPTIONS.find((option) => option.field === sortField)?.label ?? "Name"; if (!selectedCompanyId) { return ; } if (isLoading) { return ; } return (
{PROJECT_SORT_OPTIONS.map((option) => ( ))}
{error &&

{error.message}

} {!isLoading && projects.length === 0 && ( )} {projects.length > 0 && (
{([ ["My Projects", groupedProjects.mine], ["Other Projects", groupedProjects.other], ] as const).map(([label, sectionProjects]) => { if (sectionProjects.length === 0) return null; return (

{label}

{sectionProjects.length} project{sectionProjects.length === 1 ? "" : "s"}
{sectionProjects.map((project) => { const state = resourceMembershipState(membershipsQuery.data, "project", project.id); const pending = membershipMutation.isPending && membershipMutation.variables?.resourceType === "project" && membershipMutation.variables.resourceId === project.id; return ( {project.targetDate && ( {formatDate(project.targetDate)} )} membershipMutation.mutate({ resourceType: "project", resourceId: project.id, resourceName: project.name, state: "joined", })} onLeave={() => membershipMutation.mutate({ resourceType: "project", resourceId: project.id, resourceName: project.name, state: "left", })} />
} /> ); })}
); })}
)} ); }