"use client"; import * as React from "react"; import { ChevronDown } from "lucide-react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; export type TableFieldType = | "title" | "text" | "status" | "priority" | "date" | "select" | "assignees" | "created"; export interface TableCellProps { value: unknown; fieldType: TableFieldType; onChange?: (value: unknown) => void; readOnly?: boolean; className?: string; /** Options for status, priority, or custom select columns */ selectOptions?: string[]; /** Optional: move focus to next/previous cell (Tab / Shift+Tab) */ onNavigate?: (dir: "next" | "prev") => void; } function formatDateDisplay(raw: string): string { if (!raw || raw === "—") return "—"; try { const d = new Date(raw); if (Number.isNaN(d.getTime())) return raw; return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric", }); } catch { return raw; } } function formatCreatedDisplay(raw: string): string { if (!raw) return "—"; try { const d = new Date(raw); if (Number.isNaN(d.getTime())) return raw; return d.toLocaleString(undefined, { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit", }); } catch { return String(raw); } } function statusLabel(status: string | null): string { if (!status) return "—"; return String(status).replace(/_/g, " "); } function initials(name: string | null): string { return (name ?? "?") .split(/\s+/) .map((p) => p[0]) .join("") .slice(0, 2) .toUpperCase(); } type AssigneeLike = { user: { id: string; name: string | null; avatarUrl: string | null } }; function isAssigneeArray(v: unknown): v is AssigneeLike[] { return Array.isArray(v) && v.every((x) => x && typeof x === "object" && "user" in x); } export function TableCell({ value, fieldType, onChange, readOnly = false, className, selectOptions = [], onNavigate, }: TableCellProps) { const [editing, setEditing] = React.useState(false); const [draft, setDraft] = React.useState(""); const inputRef = React.useRef(null); const stringValue = React.useMemo(() => { if (value === null || value === undefined) return ""; if (typeof value === "string") return value; return String(value); }, [value]); React.useEffect(() => { if (!editing) return; if (fieldType === "date") { const raw = stringValue; if (raw && raw !== "—") { const d = new Date(raw); if (!Number.isNaN(d.getTime())) { setDraft(d.toISOString().slice(0, 10)); return; } } setDraft(""); return; } setDraft(stringValue); }, [editing, fieldType, stringValue]); React.useEffect(() => { if (editing && fieldType !== "assignees" && fieldType !== "created") { const id = requestAnimationFrame(() => inputRef.current?.focus()); return () => cancelAnimationFrame(id); } }, [editing, fieldType]); const commit = React.useCallback(() => { if (!onChange) { setEditing(false); return; } if (fieldType === "date") { onChange(draft || null); } else if (fieldType === "title" || fieldType === "text" || fieldType === "select") { onChange(draft); } else if (fieldType === "status" || fieldType === "priority") { onChange(draft); } setEditing(false); }, [draft, fieldType, onChange]); const cancel = React.useCallback(() => { setEditing(false); }, []); const startEdit = React.useCallback(() => { if (readOnly || fieldType === "assignees") return; if (fieldType === "created") return; if (fieldType === "status" || fieldType === "priority" || fieldType === "select") return; setEditing(true); }, [readOnly, fieldType]); const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); commit(); } else if (e.key === "Escape") { e.preventDefault(); cancel(); } else if (e.key === "Tab" && onNavigate) { e.preventDefault(); commit(); onNavigate(e.shiftKey ? "prev" : "next"); } }; const displayNode = (() => { if (fieldType === "assignees" && isAssigneeArray(value)) { const list = value; if (list.length === 0) { return ; } return (
{list.slice(0, 4).map((a) => ( {initials(a.user.name)} ))}
); } if (fieldType === "status") { const s = typeof value === "string" ? value : value === null ? null : String(value); return ( {statusLabel(s)} ); } if (fieldType === "priority") { return ( {stringValue || "—"} ); } if (fieldType === "date") { return ( {formatDateDisplay(stringValue || "—")} ); } if (fieldType === "created") { return ( {formatCreatedDisplay(stringValue)} ); } return ( {stringValue || "—"} ); })(); if (readOnly || fieldType === "assignees" || fieldType === "created") { return (
{displayNode}
); } if (fieldType === "status" || fieldType === "priority" || fieldType === "select") { const opts = fieldType === "status" ? ["open", "in_progress", "done", "closed"] : fieldType === "priority" ? ["High", "Medium", "Low"] : selectOptions; return (
{opts.map((opt) => ( onChange?.(opt)} > {fieldType === "status" ? statusLabel(opt) : opt} ))}
); } if (fieldType === "date") { if (editing) { return (
setDraft(e.target.value)} onBlur={commit} onKeyDown={onKeyDown} className="h-7 border-0 bg-transparent px-2 py-0 text-xs shadow-none focus-visible:ring-1" />
); } return ( ); } if (editing) { return (
setDraft(e.target.value)} onBlur={commit} onKeyDown={onKeyDown} className="h-7 border-0 bg-transparent px-2 py-0 text-sm shadow-none focus-visible:ring-1" />
); } return ( ); }