"use client"; import React, { useState, useEffect, useRef, useCallback } from "react"; import { useI18n } from "@/lib/i18n"; import { ContextMenu, MenuItem } from "@/components/ui/ContextMenu"; export interface Task { id: string; listId: string; parentId: string | null; title: string; note: string | null; completed: boolean; completedAt: string | null; dueDate: string | null; priority: number; sortOrder: number; createdAt: string; updatedAt: string; children: Task[]; tags: { tag: { id: string; name: string; color: string } }[]; } export interface List { id: string; name: string; color: string; icon: string } export interface User { id: string; name?: string | null; email?: string | null } const PRIORITY_COLORS = ["transparent", "var(--priority-low)", "var(--priority-medium)", "var(--priority-high)"]; function isOverdue(d: string | null) { if (!d) return false; return new Date(d) < new Date() && new Date(d).toDateString() !== new Date().toDateString(); } interface TaskItemProps { task: Task; isSelected: boolean; onSelect: (t: Task) => void; onToggle: (id: string, completed: boolean) => void; onUpdateTitle: (id: string, title: string) => void; onAddSubtask: (title: string, parentId: string) => void; onContextMenu: (x: number, y: number, task: Task) => void; onDeleteTask?: (id: string) => void; } function TaskItem({ task, isSelected, onSelect, onToggle, onUpdateTitle, onAddSubtask, onContextMenu, }: TaskItemProps) { const { t, lang } = useI18n(); const [expanded, setExpanded] = useState(true); const [editingTitle, setEditingTitle] = useState(false); const [tempTitle, setTempTitle] = useState(task.title); const [addingSubtask, setAddingSubtask] = useState(false); const [subtaskInput, setSubtaskInput] = useState(""); const editInputRef = useRef(null); const subInputRef = useRef(null); const completedChildren = task.children?.filter((c) => c.completed).length || 0; useEffect(() => { setTempTitle(task.title); }, [task.title]); useEffect(() => { if (editingTitle) { editInputRef.current?.focus(); editInputRef.current?.select(); } }, [editingTitle]); useEffect(() => { if (addingSubtask) { subInputRef.current?.focus(); } }, [addingSubtask]); const formatDate = (d: string | null) => { if (!d) return null; const date = new Date(d); const now = new Date(); const isToday = date.toDateString() === now.toDateString(); const isTomorrow = date.toDateString() === new Date(now.getTime() + 86400000).toDateString(); if (isToday) return t("today"); if (isTomorrow) return t("tomorrow"); return date.toLocaleDateString(lang === "ko" ? "ko-KR" : lang === "ja" ? "ja-JP" : "en-US", { month: "short", day: "numeric" }); }; const priorityLabels = [t("priorityNone"), t("priorityLow"), t("priorityMedium"), t("priorityHigh")]; const handleSaveTitle = () => { const trimmed = tempTitle.trim(); if (trimmed && trimmed !== task.title) { onUpdateTitle(task.id, trimmed); } else { setTempTitle(task.title); } setEditingTitle(false); }; const handleCreateSubtask = (e: React.FormEvent) => { e.preventDefault(); const trimmed = subtaskInput.trim(); if (trimmed) { onAddSubtask(trimmed, task.id); setSubtaskInput(""); setAddingSubtask(false); setExpanded(true); } }; return (
onSelect(task)} onContextMenu={(e) => { e.preventDefault(); onContextMenu(e.clientX, e.clientY, task); }} id={`task-${task.id}`} style={{ group: "item" } as React.CSSProperties} >
{task.note && !task.completed && (
{task.note.replace(/[#*`]/g, "").slice(0, 80)}
)}
{/* Quick Edit icon on hover */} {/* Sub-tasks List */} {((task.children && task.children.length > 0) || addingSubtask) && expanded && (
{task.children?.map((child) => ( ))} {/* Inline Add Subtask input */} {addingSubtask && (
setSubtaskInput(e.target.value)} onBlur={() => { if (!subtaskInput.trim()) setAddingSubtask(false); }} onKeyDown={(e) => { if (e.key === "Escape") setAddingSubtask(false); }} style={{ height: 26, fontSize: 12.5, padding: "2px 8px", flex: 1 }} />
)}
)} ); } // Sub-task Item with Inline Rename function SubTaskItem({ child, onToggle, onSelect, onUpdateTitle, }: { child: Task; onToggle: (id: string, completed: boolean) => void; onSelect: (t: Task) => void; onUpdateTitle: (id: string, title: string) => void; }) { const [editing, setEditing] = useState(false); const [temp, setTemp] = useState(child.title); const ref = useRef(null); useEffect(() => { if (editing) { ref.current?.focus(); ref.current?.select(); } }, [editing]); const handleSave = () => { const trimmed = temp.trim(); if (trimmed && trimmed !== child.title) { onUpdateTitle(child.id, trimmed); } else { setTemp(child.title); } setEditing(false); }; return (
onSelect(child)} id={`subtask-${child.id}`} >
); } interface Props { user: User; listId: string | null; lists: List[]; tasks: Task[]; setTasks: React.Dispatch>; selectedTaskId: string | null; onTaskSelect: (t: Task | null) => void; showCompleted: boolean; onToggleCompleted: () => void; onMenuOpen: () => void; onRefresh: () => void; isDemo?: boolean; onDemoAddTask?: (title: string, listId: string, parentId?: string | null) => void; onDemoToggleTask?: (id: string, completed: boolean) => void; onUpdateTaskTitle?: (id: string, title: string) => void; onUpdateListName?: (id: string, name: string) => void; } export function TaskList({ user, listId, lists, tasks, setTasks, selectedTaskId, onTaskSelect, showCompleted, onToggleCompleted, onMenuOpen, onRefresh, isDemo = false, onDemoAddTask, onDemoToggleTask, onUpdateTaskTitle, onUpdateListName, }: Props) { const { t } = useI18n(); const [newTaskTitle, setNewTaskTitle] = useState(""); const [loading, setLoading] = useState(false); const [editingHeader, setEditingHeader] = useState(false); const [headerTitle, setHeaderTitle] = useState(""); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; task: Task } | null>(null); const inputRef = useRef(null); const headerInputRef = useRef(null); const currentList = lists.find((l) => l.id === listId); useEffect(() => { if (currentList) setHeaderTitle(currentList.name); }, [currentList]); useEffect(() => { if (editingHeader) { headerInputRef.current?.focus(); headerInputRef.current?.select(); } }, [editingHeader]); const fetchTasks = useCallback(async () => { if (!listId || isDemo) return; setLoading(true); try { const res = await fetch(`/api/tasks?listId=${listId}&showCompleted=${showCompleted}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setTasks(Array.isArray(data) ? data : []); } catch (err) { console.error("[TaskList] fetchTasks failed", err); } finally { setLoading(false); } }, [listId, showCompleted, isDemo, setTasks]); useEffect(() => { fetchTasks(); }, [fetchTasks]); useEffect(() => { const handler = () => inputRef.current?.focus(); document.addEventListener("checkflow:addTask", handler); return () => document.removeEventListener("checkflow:addTask", handler); }, []); const handleToggle = useCallback( async (id: string, completed: boolean) => { if (isDemo) { if (onDemoToggleTask) onDemoToggleTask(id, completed); return; } try { const res = await fetch(`/api/tasks/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ completed }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const updated = await res.json(); setTasks((prev) => prev .map((t) => { if (t.id === id) return { ...t, completed, completedAt: updated.completedAt }; if (t.children && t.children.some((c) => c.id === id)) { return { ...t, children: t.children.map((c) => (c.id === id ? { ...c, completed } : c)), }; } return t; }) .filter((t) => showCompleted || !t.completed) ); onRefresh(); } catch (err) { console.error("[TaskList] handleToggle failed", err); } }, [isDemo, onDemoToggleTask, showCompleted, onRefresh, setTasks] ); const handleAddTask = async (e: React.FormEvent) => { e.preventDefault(); const title = newTaskTitle.trim(); if (!title || !listId) return; if (isDemo) { if (onDemoAddTask) onDemoAddTask(title, listId, null); setNewTaskTitle(""); return; } try { const res = await fetch("/api/tasks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, listId }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const task = await res.json(); setTasks((prev) => [...prev, task]); setNewTaskTitle(""); onRefresh(); } catch (err) { console.error("[TaskList] handleAddTask failed", err); } }; const handleAddSubtaskInline = async (title: string, parentId: string) => { if (!listId) return; if (isDemo) { if (onDemoAddTask) onDemoAddTask(title, listId, parentId); return; } try { const res = await fetch("/api/tasks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, listId, parentId }), }); if (res.ok) { const sub = await res.json(); setTasks((prev) => prev.map((t) => (t.id === parentId ? { ...t, children: [...(t.children || []), sub] } : t)) ); onRefresh(); } } catch (err) { console.error("Failed to add subtask inline", err); } }; const handleSaveHeaderTitle = () => { const trimmed = headerTitle.trim(); if (trimmed && listId && onUpdateListName && trimmed !== currentList?.name) { onUpdateListName(listId, trimmed); } else if (currentList) { setHeaderTitle(currentList.name); } setEditingHeader(false); }; if (!listId) { return (

{t("selectListToStart")}

); } const incompleteTasks = tasks.filter((t) => !t.completed); const completedTasks = tasks.filter((t) => t.completed); return (
{/* Header */}
{/* Inline Editable List Title */} {editingHeader ? ( setHeaderTitle(e.target.value)} onBlur={handleSaveHeaderTitle} onKeyDown={(e) => { if (e.key === "Enter") handleSaveHeaderTitle(); if (e.key === "Escape") { if (currentList) setHeaderTitle(currentList.name); setEditingHeader(false); } }} onClick={(e) => e.stopPropagation()} autoFocus style={{ fontSize: 18, fontWeight: 700, height: 36, padding: "2px 10px", maxWidth: 360 }} /> ) : (
{ e.stopPropagation(); setEditingHeader(true); }} title="Click to rename list" > {currentList?.name || t("tasks")} ✏️
)}
{/* Task list */}
{loading && (
{t("loading")}
)} {!loading && incompleteTasks.length === 0 && completedTasks.length === 0 && (

{t("noTasksYet")}

)} {incompleteTasks.map((task) => ( onUpdateTaskTitle && onUpdateTaskTitle(id, title)} onAddSubtask={handleAddSubtaskInline} onContextMenu={(x, y, tItem) => setContextMenu({ x, y, task: tItem })} /> ))} {showCompleted && completedTasks.length > 0 && (
{t("completedSection")} ({completedTasks.length})
{completedTasks.map((task) => ( onUpdateTaskTitle && onUpdateTaskTitle(id, title)} onAddSubtask={handleAddSubtaskInline} onContextMenu={(x, y, tItem) => setContextMenu({ x, y, task: tItem })} /> ))}
)}
{/* Context Menu on Task */} {contextMenu && ( handleToggle(contextMenu.task.id, !contextMenu.task.completed), }, { label: t("taskTitlePlaceholder"), icon: "✏️", onClick: () => onTaskSelect(contextMenu.task), }, { label: t("subtasks"), icon: "↳", onClick: () => { onTaskSelect(contextMenu.task); }, }, { divider: true, label: "" }, { label: t("deleteTaskConfirm").split("?")[0], icon: "🗑️", danger: true, onClick: async () => { if (isDemo) { setTasks((prev) => prev.filter((t) => t.id !== contextMenu.task.id)); } else { await fetch(`/api/tasks/${contextMenu.task.id}`, { method: "DELETE" }); onRefresh(); } }, }, ]} onClose={() => setContextMenu(null)} /> )} {/* Add task bar */}
)}
); }