"use client"; import React, { useState, useEffect, useRef, useCallback } from "react"; import { useI18n } from "@/lib/i18n"; import { ContextMenu } from "@/components/ui/ContextMenu"; import { KanbanView } from "./KanbanView"; import { useUserPrefs } from "@/lib/useUserPrefs"; import { getDemoStore, saveDemoStore, MockTask } from "@/lib/mockData"; export interface Tag { id: string; name: string; color: string } 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; deletedAt?: string | null; isDeleted?: boolean; children: Task[]; tags?: { tag: Tag }[]; } 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)"]; interface TaskItemProps { task: Task; depth?: number; 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; isTrashMode?: boolean; onRestore?: (id: string) => void; onPermanentDelete?: (id: string) => void; onDragTask?: (draggedId: string, targetId: string, position: "before" | "after" | "inside") => void; } // Recursive Task Tree Item (Supports 1st, 2nd, 3rd, N-level sub-tasks seamlessly) function RecursiveTaskItem({ task, depth = 0, isSelected, onSelect, onToggle, onUpdateTitle, onAddSubtask, onContextMenu, isTrashMode = false, onRestore, onPermanentDelete, onDragTask, }: 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(""); // Touch swipe support const touchStartX = useRef(null); const [swipeOffset, setSwipeOffset] = useState(0); const editInputRef = useRef(null); const subInputRef = useRef(null); const completedChildren = task.children?.filter((c) => c.completed).length || 0; const totalChildren = task.children?.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]); // 마감일 상태 반환: label + 색상 const getDueDateInfo = (d: string | null): { label: string; color: 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(); const isPast = date < now && !isToday; if (isToday) return { label: t("today"), color: "#EF4444" }; if (isTomorrow) return { label: t("tomorrow"), color: "#F97316" }; if (isPast) return { label: date.toLocaleDateString(lang === "ko" ? "ko-KR" : lang === "ja" ? "ja-JP" : "en-US", { month: "short", day: "numeric" }), color: "#DC2626" }; return { label: date.toLocaleDateString(lang === "ko" ? "ko-KR" : lang === "ja" ? "ja-JP" : "en-US", { month: "short", day: "numeric" }), color: "var(--text-tertiary)" }; }; 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); } }; // Touch Swipe handlers const handleTouchStart = (e: React.TouchEvent) => { if (isTrashMode) return; touchStartX.current = e.touches[0].clientX; }; const handleTouchMove = (e: React.TouchEvent) => { if (touchStartX.current === null) return; const deltaX = e.touches[0].clientX - touchStartX.current; if (Math.abs(deltaX) < 120) { setSwipeOffset(deltaX); } }; const handleTouchEnd = () => { if (swipeOffset > 70) { // Swiped Right -> Toggle Complete onToggle(task.id, !task.completed); } setSwipeOffset(0); touchStartX.current = null; }; const [dragOverPos, setDragOverPos] = useState<"top" | "bottom" | "inside" | null>(null); return (
0 ? 24 : 0, position: "relative" }}>
onSelect(task)} tabIndex={0} onKeyDown={(e) => { if (e.key === "F2" && !isTrashMode) { e.preventDefault(); e.stopPropagation(); setEditingTitle(true); } }} onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(e.clientX, e.clientY, task); }} onDragOver={(e) => { if (isTrashMode) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; const rect = e.currentTarget.getBoundingClientRect(); const relY = e.clientY - rect.top; const h = rect.height; if (relY < h * 0.25) { setDragOverPos("top"); } else if (relY > h * 0.75) { setDragOverPos("bottom"); } else { setDragOverPos("inside"); } }} onDragLeave={() => setDragOverPos(null)} onDrop={(e) => { if (isTrashMode) return; e.preventDefault(); const draggedId = e.dataTransfer.getData("text/plain"); if (draggedId && draggedId !== task.id && onDragTask) { const pos: "before" | "after" | "inside" = dragOverPos === "top" ? "before" : dragOverPos === "bottom" ? "after" : "inside"; onDragTask(draggedId, task.id, pos); } setDragOverPos(null); }} id={`task-${task.id}`} style={{ borderLeft: depth > 0 ? "2px solid var(--border)" : "none", marginLeft: depth > 0 ? 8 : 0, transform: `translateX(${swipeOffset}px)`, transition: swipeOffset === 0 ? "transform 0.2s cubic-bezier(0.16, 1, 0.3, 1)" : "none", outline: dragOverPos === "inside" ? "2px dashed var(--accent)" : undefined, outlineOffset: -2, background: dragOverPos === "inside" ? "var(--accent-light, rgba(75, 123, 245, 0.12))" : undefined, }} > {/* Notion-style 6-dot Drag Handle */} {!isTrashMode && (
{ e.stopPropagation(); e.dataTransfer.setData("text/plain", task.id); e.dataTransfer.setData("application/task-id", task.id); e.dataTransfer.effectAllowed = "move"; }} title="Drag to reorder" onClick={(e) => e.stopPropagation()} >
)} {/* Toggle Expand Arrow if has children */} {totalChildren > 0 ? ( ) : depth > 0 ? ( ) : null} {/* Check button (hidden in trash mode) */} {!isTrashMode ? (
) : (
)}
{/* Recursive Children (N-Depth Sub-tasks) */} {totalChildren > 0 && expanded && (
{task.children.map((child) => ( ))}
)} {/* Inline Add Subtask form for this node */} {addingSubtask && !isTrashMode && (
e.stopPropagation()} > 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 }} />
)} ); } 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; isTrashActive?: boolean; selectedTag?: string | null; onEmptyTrash?: () => void; onRestoreTask?: (id: string) => void; onPermanentDeleteTask?: (id: string) => void; onDemoAddTask?: ( title: string, listId: string, parentId?: string | null, meta?: { dueDate?: string | null; priority?: number; tags?: { tag: Tag }[] } ) => void; onDemoToggleTask?: (id: string, completed: boolean) => void; onUpdateTaskTitle?: (id: string, title: string) => void; onUpdateListName?: (id: string, name: string) => void; onDeleteTaskWithUndo?: (task: Task) => void; onDeleteTask?: (id: string) => void; } export function TaskList({ user: _user, listId, lists, tasks, setTasks, selectedTaskId, onTaskSelect, showCompleted, onToggleCompleted, onMenuOpen, onRefresh, isDemo = false, isTrashActive = false, selectedTag = null, onEmptyTrash, onRestoreTask, onPermanentDeleteTask, onDemoAddTask, onDemoToggleTask, onUpdateTaskTitle, onUpdateListName, onDeleteTaskWithUndo, onDeleteTask, }: 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 { prefs, updatePrefs } = useUserPrefs(); // Only show kanban if the Labs flag is explicitly enabled const kanbanEnabled = Boolean(prefs.labs?.kanbanBoard); const viewMode = kanbanEnabled ? (prefs.viewMode || "list") : "list"; const handleToggleViewMode = () => { const nextMode = viewMode === "kanban" ? "list" : "kanban"; updatePrefs({ viewMode: nextMode }); }; // TickTick-style Quick Add Preset states const [quickDueDate, setQuickDueDate] = useState(null); const [quickPriority, setQuickPriority] = useState(0); const [showQuickDue, setShowQuickDue] = useState(false); const [showQuickPriority, setShowQuickPriority] = useState(false); const priorityLabels = [t("priorityNone"), t("priorityLow"), t("priorityMedium"), t("priorityHigh")]; 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 || isTrashActive || selectedTag) 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, isTrashActive, selectedTag, setTasks]); useEffect(() => { fetchTasks(); }, [fetchTasks]); useEffect(() => { const handler = () => inputRef.current?.focus(); document.addEventListener("checkflow:addTask", handler); return () => document.removeEventListener("checkflow:addTask", handler); }, []); // Recursive update helper for local tree optimistic state const updateTaskCompletedInTree = (tree: Task[], targetId: string, isComp: boolean): Task[] => { return tree.map((node) => { if (node.id === targetId) { return { ...node, completed: isComp, completedAt: isComp ? new Date().toISOString() : null, children: node.children ? updateTaskCompletedInTree(node.children, targetId, isComp) : [], }; } if (node.children && node.children.length > 0) { return { ...node, children: updateTaskCompletedInTree(node.children, targetId, isComp), }; } return node; }); }; const handleToggle = useCallback( async (id: string, completed: boolean) => { // 1. Optimistic UI update immediately setTasks((prev) => { const updated = updateTaskCompletedInTree(prev, id, completed); return showCompleted ? updated : updated.filter((t) => t.id !== id || !completed); }); 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}`); onRefresh(); } catch (err) { console.error("[TaskList] handleToggle failed", err); // Rollback on failure fetchTasks(); } }, [isDemo, onDemoToggleTask, onRefresh, showCompleted, setTasks, fetchTasks] ); const handleAddTask = async (e: React.FormEvent) => { e.preventDefault(); const title = newTaskTitle.trim(); if (!title || !listId) return; const meta = { dueDate: quickDueDate, priority: quickPriority, }; if (isDemo) { if (onDemoAddTask) onDemoAddTask(title, listId, null, meta); setNewTaskTitle(""); setQuickDueDate(null); setQuickPriority(0); return; } try { const res = await fetch("/api/tasks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, listId, ...meta }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const task = await res.json(); setTasks((prev) => [...prev, task]); setNewTaskTitle(""); setQuickDueDate(null); setQuickPriority(0); onRefresh(); } catch (err) { console.error("[TaskList] handleAddTask failed", err); } }; const insertSubtaskInTree = (tree: Task[], pId: string, subtask: Task): Task[] => { return tree.map((node) => { if (node.id === pId) { const currentChildren = node.children || []; return { ...node, children: [...currentChildren, subtask] }; } if (node.children && node.children.length > 0) { return { ...node, children: insertSubtaskInTree(node.children, pId, subtask) }; } return node; }); }; const handleAddSubtaskInline = async (title: string, parentId: string) => { if (!listId) return; const tempId = "temp-subtask-" + Date.now(); const tempSubtask: Task = { id: tempId, listId, parentId, title: title.trim(), note: null, completed: false, completedAt: null, dueDate: null, priority: 0, sortOrder: 999, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), children: [], tags: [], }; // Optimistic insert setTasks((prev) => insertSubtaskInTree(prev, parentId, tempSubtask)); 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 createdTask = await res.json(); // Replace temp subtask with actual created task setTasks((prev) => { const replaceTemp = (nodes: Task[]): Task[] => { return nodes.map((n) => { if (n.id === tempId) return createdTask; if (n.children && n.children.length > 0) { return { ...n, children: replaceTemp(n.children) }; } return n; }); }; return replaceTemp(prev); }); onRefresh(); } else { fetchTasks(); } } catch (err) { console.error("Failed to add subtask inline", err); fetchTasks(); } }; const handleSaveHeaderTitle = () => { const trimmed = headerTitle.trim(); if (trimmed && listId && onUpdateListName && trimmed !== currentList?.name) { onUpdateListName(listId, trimmed); } else if (currentList) { setHeaderTitle(currentList.name); } setEditingHeader(false); }; // Helper: check if targetId is inside the subtree of ancestorId (prevents circular nesting / node disappearing) const isDescendantNode = (nodes: Task[], ancestorId: string, targetId: string): boolean => { for (const node of nodes) { if (node.id === ancestorId) { const checkChildren = (children: Task[]): boolean => { for (const c of children) { if (c.id === targetId) return true; if (c.children && c.children.length > 0 && checkChildren(c.children)) return true; } return false; }; return checkChildren(node.children || []); } if (node.children && node.children.length > 0) { if (isDescendantNode(node.children, ancestorId, targetId)) return true; } } return false; }; // Robust Tree-aware reordering function (1st, 2nd, 3rd depth task reordering, promoting to top-level, and demoting into subtask) const handleReorderTasks = useCallback((draggedId: string, targetId: string, position: "before" | "after" | "inside") => { // Prevent dropping onto itself or into its own subtree (which causes loops/disappearing tasks) if (draggedId === targetId) return; setTasks((prev) => { if (isDescendantNode(prev, draggedId, targetId)) { return prev; } // 1. Extract and remove the dragged node from its current position let extractedNode: Task | null = null; const removeNode = (nodes: Task[]): Task[] => { const result: Task[] = []; for (const node of nodes) { if (node.id === draggedId) { extractedNode = { ...node }; } else { const updatedNode = { ...node }; if (updatedNode.children && updatedNode.children.length > 0) { updatedNode.children = removeNode(updatedNode.children); } result.push(updatedNode); } } return result; }; const treeWithoutDragged = removeNode(prev); if (!extractedNode) return prev; const safeExtractedNode: Task = extractedNode; let determinedParentId: string | null = null; // 2. Insert into the target position if (position === "inside") { determinedParentId = targetId; const insertInside = (nodes: Task[]): { list: Task[]; inserted: boolean } => { let hasInserted = false; const newNodes = nodes.map((node) => { if (node.id === targetId) { hasInserted = true; const nodeWithNewParent: Task = { ...safeExtractedNode, parentId: targetId }; return { ...node, children: [...(node.children || []), nodeWithNewParent], }; } if (node.children && node.children.length > 0) { const res = insertInside(node.children); if (res.inserted) { hasInserted = true; return { ...node, children: res.list }; } } return node; }); return { list: newNodes, inserted: hasInserted }; }; const result = insertInside(treeWithoutDragged); const finalTree = result.inserted ? result.list : [...treeWithoutDragged, { ...safeExtractedNode, parentId: null }]; if (isDemo && typeof window !== "undefined") { const store = getDemoStore(); saveDemoStore(store.lists, finalTree as unknown as MockTask[]); } else { fetch(`/api/tasks/${draggedId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parentId: determinedParentId }), }).catch((err) => console.error("Failed to update parentId", err)); } return finalTree; } else { // Drop before/after as sibling (if target is at root level, parentId becomes null -> promoted to 1st level) const insertSibling = (nodes: Task[], currentParentId: string | null): { list: Task[]; inserted: boolean } => { const idx = nodes.findIndex((n) => n.id === targetId); if (idx !== -1) { determinedParentId = currentParentId; const insertIdx = position === "before" ? idx : idx + 1; const nodeWithNewParent: Task = { ...safeExtractedNode, parentId: currentParentId }; const newNodes = [...nodes]; newNodes.splice(insertIdx, 0, nodeWithNewParent); return { list: newNodes, inserted: true }; } let hasInserted = false; const newNodes = nodes.map((node) => { if (!hasInserted && node.children && node.children.length > 0) { const res = insertSibling(node.children, node.id); if (res.inserted) { hasInserted = true; return { ...node, children: res.list }; } } return node; }); return { list: newNodes, inserted: hasInserted }; }; const result = insertSibling(treeWithoutDragged, null); const finalTree = result.inserted ? result.list : [...treeWithoutDragged, { ...safeExtractedNode, parentId: null }]; if (isDemo && typeof window !== "undefined") { const store = getDemoStore(); saveDemoStore(store.lists, finalTree as unknown as MockTask[]); } else { fetch(`/api/tasks/${draggedId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parentId: determinedParentId }), }).catch((err) => console.error("Failed to update sibling parentId", err)); } return finalTree; } }); }, [isDemo, setTasks]); const handleDeleteTask = useCallback((task: Task) => { if (onDeleteTaskWithUndo) { onDeleteTaskWithUndo(task); } else if (onDeleteTask) { onDeleteTask(task.id); } }, [onDeleteTaskWithUndo, onDeleteTask]); if (!listId && !isTrashActive && !selectedTag) { return (

{t("selectListToStart")}

); } // Filter top-level vs completed const incompleteTasks = tasks.filter((t) => !t.completed); const completedTasks = tasks.filter((t) => t.completed); return (
{/* Header */}
{/* Header Title / Trash Header / Tag Header */} {isTrashActive ? (
🗑️ {t("trash") || "Trash"} ({tasks.length})
) : selectedTag ? (
🏷️ #{selectedTag} ({tasks.length})
) : 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", width: "100%", maxWidth: 320 }} /> ) : (
{ e.stopPropagation(); setEditingHeader(true); }} title="Click to rename list" > {currentList?.name || t("tasks")} ({incompleteTasks.length})
)}
{/* Header Actions */}
{/* View Switcher: Minimal & Matte Single Toggle (Only shown when Kanban is enabled in Labs) */} {!isTrashActive && !selectedTag && kanbanEnabled && ( )} {isTrashActive ? ( ) : ( )}
{/* Main Task View (List View vs Kanban View) */} {viewMode === "kanban" && !isTrashActive && !selectedTag ? (
{ if (!listId) return; if (isDemo && onDemoAddTask) { onDemoAddTask(title, listId, null, { priority }); } else if (!isDemo) { fetch("/api/tasks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, listId, priority }), }).then(() => onRefresh()); } }} />
) : ( /* Task list container */
{ if (e.target === e.currentTarget) { e.preventDefault(); } }} > {loading && (
{t("loading")}
)} {!loading && tasks.length === 0 && (

{isTrashActive ? "Trash is empty" : selectedTag ? "No tasks with this tag" : t("noTasksYet")}

)} {/* Tasks Tree */} {(isTrashActive ? tasks : incompleteTasks).map((task) => ( onUpdateTaskTitle && onUpdateTaskTitle(id, title)} onAddSubtask={handleAddSubtaskInline} onContextMenu={(x, y, tItem) => setContextMenu({ x, y, task: tItem })} isTrashMode={isTrashActive} onRestore={onRestoreTask} onPermanentDelete={onPermanentDeleteTask} onDragTask={handleReorderTasks} /> ))} {/* Completed Tasks */} {!isTrashActive && 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 })} isTrashMode={isTrashActive} onRestore={onRestoreTask} onPermanentDelete={onPermanentDeleteTask} onDragTask={handleReorderTasks} /> ))}
)}
)} {/* Full-featured Context Menu */} {contextMenu && !isTrashActive && ( 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: () => handleDeleteTask(contextMenu.task), }, ]} onClose={() => setContextMenu(null)} /> )} {/* Add task bar with TickTick-style Quick Presets (hidden in trash mode) */} {!isTrashActive && !selectedTag && (
)}
{/* TickTick-style Preset Toolbar (Due Date, Priority) */}
{/* Due Date Preset */}
{showQuickDue && (
e.stopPropagation()} >
{ setQuickDueDate(new Date().toISOString().split("T")[0]); setShowQuickDue(false); }} > ⚡ {t("quickToday")}
{ const tm = new Date(Date.now() + 86400000).toISOString().split("T")[0]; setQuickDueDate(tm); setShowQuickDue(false); }} > 🌅 {t("quickTomorrow")}
{ const nw = new Date(Date.now() + 7 * 86400000).toISOString().split("T")[0]; setQuickDueDate(nw); setShowQuickDue(false); }} > 🗓️ {t("quickNextWeek")}
{ setQuickDueDate(e.target.value || null); setShowQuickDue(false); }} />
{quickDueDate && (
{ setQuickDueDate(null); setShowQuickDue(false); }} > ✕ {t("clear")}
)}
)}
{/* Priority Preset */}
{showQuickPriority && (
e.stopPropagation()} > {[0, 1, 2, 3].map((pVal) => (
0 ? PRIORITY_COLORS[pVal] : "inherit", fontWeight: quickPriority === pVal ? 700 : 400, }} onClick={() => { setQuickPriority(pVal); setShowQuickPriority(false); }} > {pVal === 0 ? `○ ${t("priorityNone")}` : `● ${priorityLabels[pVal]}`}
))}
)}
{/* Quick Helper Text */} {(quickDueDate || quickPriority > 0) && ( )}
)}
); }