"use client"; import type { DragEndEvent, DragStartEvent } from "@dnd-kit/core"; import { closestCenter, DndContext, DragOverlay, KeyboardSensor, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { ArrowDown, ArrowUp, Calendar, Check, CheckCircle2, ChevronDown, ChevronRight, Circle, Copy, Flag, GripVertical, List, MoreVertical, Move, Plus, Puzzle, SendHorizonal, Star, Trash2, X, } from "lucide-react"; import { useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "@/components/ui/empty"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, } from "@/components/ui/input-group"; import { Item, ItemContent, ItemGroup, ItemMedia, ItemTitle, } from "@/components/ui/item"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; const formatDueDate = (date: Date) => date.toLocaleDateString("fa-IR", { month: "short", day: "numeric" }); interface Todo10Labels { moreActions: string; addTaskAbove: string; addTaskBelow: string; dateSection: string; prioritySection: string; today: string; tomorrow: string; nextWeek: string; nextMonth: string; noDate: string; moveTo: string; duplicate: string; copyTaskLink: string; addExtension: string; removeFavorite: string; addFavorite: string; delete: string; cancel: string; deleteDialogTitle: string; deleteDialogDescription: (title: string) => string; addTaskLabel: string; taskPlaceholder: string; dateLabel: string; priorityLabel: string; noPriority: string; selectProject: string; submitTask: string; emptyTitle: string; emptyDescription: string; allTasks: (count: number) => string; completedLabel: string; expandSubtasks: string; collapseSubtasks: string; dragTask: (title: string) => string; markIncomplete: (title: string) => string; markComplete: (title: string) => string; } const defaultLabels: Todo10Labels = { moreActions: "گزینه‌های بیشتر", addTaskAbove: "افزودن وظیفه بالا", addTaskBelow: "افزودن وظیفه پایین", dateSection: "تاریخ", prioritySection: "اولویت", today: "امروز", tomorrow: "فردا", nextWeek: "هفته آینده", nextMonth: "ماه آینده", noDate: "بدون تاریخ", moveTo: "انتقال به…", duplicate: "تکثیر", copyTaskLink: "کپی لینک وظیفه", addExtension: "افزودن افزونه…", removeFavorite: "حذف از علاقه‌مندی‌ها", addFavorite: "افزودن به علاقه‌مندی‌ها", delete: "حذف", cancel: "انصراف", deleteDialogTitle: "حذف وظیفه؟", deleteDialogDescription: (title) => `وظیفه «${title}» برای همیشه حذف می‌شود.`, addTaskLabel: "افزودن وظیفه جدید", taskPlaceholder: "عنوان وظیفه", dateLabel: "تاریخ", priorityLabel: "اولویت", noPriority: "بدون اولویت", selectProject: "انتخاب پروژه", submitTask: "افزودن", emptyTitle: "هنوز وظیفه‌ای ندارید", emptyDescription: "با افزودن اولین وظیفه خود شروع کنید.", allTasks: (count) => `همه وظایف (${count})`, completedLabel: "تکمیل‌شده", expandSubtasks: "نمایش زیروظایف", collapseSubtasks: "پنهان کردن زیروظایف", dragTask: (title) => `جابه‌جایی «${title}»`, markIncomplete: (title) => `علامت‌گذاری «${title}» به‌عنوان انجام‌نشده`, markComplete: (title) => `علامت‌گذاری «${title}» به‌عنوان انجام‌شده`, }; interface Todo10Props { projects?: Project[]; initialTodos?: Todo[]; labels?: Todo10Labels; className?: string; } type Priority = "P1" | "P2" | "P3" | "P4"; type SortOption = | "manual" | "dueDate" | "priority" | "title" | "createdAt" | "updatedAt"; interface Subtask { id: string; title: string; completed: boolean; } interface Project { id: string; name: string; color?: string; } interface Todo { id: string; title: string; completed: boolean; projectId: string; priority?: Priority; dueDate?: Date; starred?: boolean; subtasks: Subtask[]; createdAt: Date; updatedAt: Date; } interface TodoFilters { projects: string[]; priorities: string[]; starredOnly: boolean; } const defaultProjects: Project[] = [ { id: "personal", name: "کارهای شخصی", color: "#3b82f6" }, { id: "work", name: "پروژه‌های کاری", color: "#8b5cf6" }, { id: "shopping", name: "خرید", color: "#10b981" }, { id: "learning", name: "یادگیری", color: "#ec4899" }, ]; const defaultTodos: Todo[] = [ { id: "1", title: "تکمیل مستندات فنی پروژه", completed: false, projectId: "work", priority: "P1", dueDate: new Date(2025, 9, 25), starred: true, subtasks: [ { id: "s1", title: "نوشتن مستندات فنی", completed: true }, { id: "s2", title: "افزودن نمونه کد", completed: false }, ], createdAt: new Date(2025, 9, 15), updatedAt: new Date(2025, 9, 20), }, { id: "2", title: "بررسی و تأیید طراحی رابط کاربری", completed: false, projectId: "work", priority: "P2", subtasks: [], createdAt: new Date(2025, 9, 18), updatedAt: new Date(2025, 9, 20), }, { id: "3", title: "آماده‌سازی گزارش هفتگی", completed: false, projectId: "shopping", priority: "P3", subtasks: [], createdAt: new Date(2025, 9, 20), updatedAt: new Date(2025, 9, 20), }, { id: "4", title: "هماهنگی جلسات تیم", completed: true, projectId: "personal", priority: "P2", subtasks: [], createdAt: new Date(2024, 9, 19), updatedAt: new Date(2024, 9, 23), }, { id: "5", title: "بررسی مستندات فنی", completed: false, projectId: "learning", priority: "P4", starred: true, subtasks: [], createdAt: new Date(2025, 9, 16), updatedAt: new Date(2025, 9, 20), }, { id: "6", title: "آماده‌سازی ارائه جلسه تیم", completed: false, projectId: "work", priority: "P2", dueDate: new Date(2025, 10, 5), subtasks: [ { id: "s3", title: "طراحی اسلایدها", completed: false }, { id: "s4", title: "آماده‌سازی محیط دمو", completed: false }, ], createdAt: new Date(2025, 9, 30), updatedAt: new Date(2025, 9, 30), }, { id: "7", title: "برنامه‌ریزی جلسه حضوری تیمی", completed: false, projectId: "personal", priority: "P3", dueDate: new Date(2025, 10, 8), starred: true, subtasks: [ { id: "s5", title: "بررسی آب‌وهوا", completed: false }, { id: "s6", title: "هماهنگی مکان", completed: false }, ], createdAt: new Date(2025, 9, 30), updatedAt: new Date(2025, 9, 30), }, { id: "8", title: "بازبینی pull request شماره ۲۳۴", completed: true, projectId: "work", priority: "P2", subtasks: [], createdAt: new Date(2024, 9, 22), updatedAt: new Date(2024, 9, 23), }, { id: "9", title: "به‌روزرسانی مستندات فنی", completed: true, projectId: "work", priority: "P3", subtasks: [], createdAt: new Date(2024, 9, 22), updatedAt: new Date(2024, 9, 23), }, { id: "10", title: "رفع مشکلات طراحی واکنش‌گرا", completed: true, projectId: "work", priority: "P1", subtasks: [], createdAt: new Date(2024, 9, 21), updatedAt: new Date(2024, 9, 22), }, ]; const priorityColors: Record = { P1: "text-red-500", P2: "text-orange-500", P3: "text-yellow-500", P4: "text-gray-500", }; const priorityLabels: Record = { P1: "P1 - فوری", P2: "P2 - زیاد", P3: "P3 - متوسط", P4: "P4 - کم", }; function sortTodos(todos: Todo[], sortBy: SortOption): Todo[] { if (sortBy === "manual") return todos; return [...todos].sort((a, b) => { switch (sortBy) { case "dueDate": if (!a.dueDate && !b.dueDate) return 0; if (!a.dueDate) return 1; if (!b.dueDate) return -1; return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime(); case "priority": { const priorityOrder = { P1: 1, P2: 2, P3: 3, P4: 4 }; const aPriority = a.priority ? priorityOrder[a.priority] : 5; const bPriority = b.priority ? priorityOrder[b.priority] : 5; return aPriority - bPriority; } case "title": return a.title.localeCompare(b.title, "fa"); case "createdAt": return ( new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); case "updatedAt": return ( new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() ); default: return 0; } }); } function filterTodos(todos: Todo[], filters: TodoFilters): Todo[] { let result = [...todos]; if (filters.starredOnly) { result = result.filter((todo) => todo.starred); } if (filters.projects.length > 0) { result = result.filter((todo) => filters.projects.includes(todo.projectId)); } if (filters.priorities.length > 0) { result = result.filter((todo) => todo.priority ? filters.priorities.includes(todo.priority) : false, ); } return result; } interface TodoActionsDropdownProps { todo: Todo; isOpen: boolean; onOpenChange: (open: boolean) => void; onDelete?: (id: string) => void; onUpdate?: (todo: Todo) => void; onToggleStar?: (id: string) => void; labels: Todo10Labels; } const TodoActionsDropdown = ({ todo, isOpen, onOpenChange, onDelete, onUpdate, onToggleStar, labels, }: TodoActionsDropdownProps) => { const [showDeleteDialog, setShowDeleteDialog] = useState(false); const dateOptions = [ { type: "today" as const, label: labels.today, color: "text-blue-500" }, { type: "tomorrow" as const, label: labels.tomorrow, color: "text-green-500" }, { type: "next-week" as const, label: labels.nextWeek, color: "text-cyan-500" }, { type: "next-month" as const, label: labels.nextMonth, color: "text-orange-500", }, { type: "no-date" as const, label: labels.noDate, color: "text-yellow-500" }, ]; const getPresetDate = ( type: "today" | "tomorrow" | "next-week" | "next-month" | "no-date", ): Date | undefined => { const now = new Date(); switch (type) { case "today": return now; case "tomorrow": { const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); return tomorrow; } case "next-week": { const nextWeek = new Date(now); nextWeek.setDate(nextWeek.getDate() + 7); return nextWeek; } case "next-month": { const nextMonth = new Date(now); nextMonth.setMonth(nextMonth.getMonth() + 1); return nextMonth; } case "no-date": return undefined; } }; const setDate = ( dateType: "today" | "tomorrow" | "next-week" | "next-month" | "no-date", ) => { if (!onUpdate) return; const newDate = getPresetDate(dateType); onUpdate({ ...todo, dueDate: newDate }); }; const setPriority = (priority: Priority) => { if (!onUpdate) return; onUpdate({ ...todo, priority }); }; const handleCopyLink = () => { const taskUrl = `${window.location.origin}/task/${todo.id}`; navigator.clipboard.writeText(taskUrl); }; return ( <> e.stopPropagation()} onCloseAutoFocus={(e) => e.preventDefault()} className="w-64" > e.stopPropagation()}> {labels.addTaskAbove} e.stopPropagation()}> {labels.addTaskBelow}
{labels.dateSection} T
{dateOptions.map((option) => ( {option.label} ))}
{labels.prioritySection} Y
{(["P1", "P2", "P3", "P4"] as Priority[]).map((priority) => ( {priorityLabels[priority]} ))}
e.stopPropagation()}> {labels.moveTo} V e.stopPropagation()}> {labels.duplicate} { e.stopPropagation(); handleCopyLink(); }} > {labels.copyTaskLink} ⇧ ⌘ L e.stopPropagation()}> {labels.addExtension} {!todo.completed && ( <> { e.stopPropagation(); onToggleStar?.(todo.id); }} > {todo.starred ? labels.removeFavorite : labels.addFavorite} )} { e.stopPropagation(); onOpenChange(false); setShowDeleteDialog(true); }} > {labels.delete} ⌘ ⌫
{labels.deleteDialogTitle} {labels.deleteDialogDescription(todo.title)} ); }; interface SortableTodoItemProps { todo: Todo; project?: Project; onToggle: (id: string) => void; onToggleSubtask: (todoId: string, subtaskId: string) => void; onToggleExpand: (id: string) => void; onDelete?: (id: string) => void; onUpdate?: (todo: Todo) => void; onToggleStar?: (id: string) => void; isExpanded: boolean; labels: Todo10Labels; } const SortableTodoItem = ({ todo, project, onToggle, onToggleSubtask, onToggleExpand, onDelete, onUpdate, onToggleStar, isExpanded, labels, }: SortableTodoItemProps) => { const [isDropdownOpen, setIsDropdownOpen] = useState(false); const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: todo.id }); const style = { transform: CSS.Translate.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; const completedSubtasks = todo.subtasks.filter((st) => st.completed).length; const totalSubtasks = todo.subtasks.length; return (
onToggle(todo.id)} >
{todo.subtasks.length > 0 && !todo.completed && ( )}
{todo.title}
{todo.dueDate && !todo.completed && (
{formatDueDate(todo.dueDate)}
)} {!todo.completed && (
)}
{project && !todo.completed && (
{project.name} {totalSubtasks > 0 && ( <> · {totalSubtasks} )}
)} {isExpanded && todo.subtasks.length > 0 && !todo.completed && (
{todo.subtasks.map((subtask) => (
{ e.stopPropagation(); onToggleSubtask(todo.id, subtask.id); }} > {subtask.title}
))}
)}
); }; interface AddTaskFormProps { projects: Project[]; selectedProjectId: string; onAddTask: ( title: string, projectId: string, dueDate?: Date, priority?: Priority, ) => void; onProjectChange: (projectId: string) => void; labels: Todo10Labels; } const AddTaskForm = ({ projects, selectedProjectId, onAddTask, onProjectChange, labels, }: AddTaskFormProps) => { const [isExpanded, setIsExpanded] = useState(false); const [title, setTitle] = useState(""); const [date, setDate] = useState(); const [priority, setPriority] = useState(); const [datePickerOpen, setDatePickerOpen] = useState(false); const selectedProject = projects.find((p) => p.id === selectedProjectId); const handleAddTask = () => { if (title.trim()) { onAddTask(title.trim(), selectedProjectId, date, priority); setTitle(""); setDate(undefined); setPriority(undefined); setIsExpanded(false); } }; const handleCancel = () => { setTitle(""); setDate(undefined); setPriority(undefined); setIsExpanded(false); }; if (!isExpanded) { return ( ); } return ( setTitle(e.target.value)} aria-label={labels.taskPlaceholder} onKeyDown={(e) => { if (e.key === "Enter" && title.trim()) { e.preventDefault(); handleAddTask(); } if (e.key === "Escape") { handleCancel(); } }} className="py-3 text-base font-medium" /> {date ? ( {formatDueDate(date)} ) : ( labels.dateLabel )}
{priority ? ( ) : ( )} {priority ? priorityLabels[priority] : labels.priorityLabel}
{(["P1", "P2", "P3", "P4"] as Priority[]).map((p) => ( ))}
{selectedProject ? selectedProject.name : labels.selectProject}
{projects.map((project) => ( ))}
{labels.cancel} {labels.submitTask}
); }; const Todo10 = ({ projects = defaultProjects, initialTodos = defaultTodos, labels = defaultLabels, className, }: Todo10Props) => { const [todos, setTodos] = useState(initialTodos); const [sortBy, setSortBy] = useState("manual"); const [filters, setFilters] = useState({ projects: [], priorities: [], starredOnly: false, }); const [selectedProjectId, setSelectedProjectId] = useState( projects[0]?.id ?? "personal", ); const [isCompletedExpanded, setIsCompletedExpanded] = useState(true); const [activeTodo, setActiveTodo] = useState(null); const [expandedTodos, setExpandedTodos] = useState>( new Set( initialTodos .filter((t) => t.subtasks.length > 0 && !t.completed) .map((t) => t.id), ), ); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const filteredTodos = useMemo(() => { return filterTodos(todos, filters); }, [todos, filters]); const { filteredActiveTodos, filteredCompletedTodos } = useMemo(() => { const completedTasks = filteredTodos.filter((todo) => todo.completed); const activeTasks = filteredTodos.filter((todo) => !todo.completed); const sortedActive = sortTodos(activeTasks, sortBy); const sortedCompleted = sortTodos(completedTasks, sortBy); return { filteredActiveTodos: sortedActive, filteredCompletedTodos: sortedCompleted, }; }, [filteredTodos, sortBy]); const toggleTodo = (id: string) => { setTodos((prevTodos) => prevTodos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo, ), ); }; const toggleSubtask = (todoId: string, subtaskId: string) => { setTodos((prevTodos) => prevTodos.map((todo) => todo.id === todoId ? { ...todo, subtasks: todo.subtasks.map((st) => st.id === subtaskId ? { ...st, completed: !st.completed } : st, ), } : todo, ), ); }; const toggleExpand = (id: string) => { setExpandedTodos((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }; const addTask = ( title: string, projectId: string, dueDate?: Date, priority?: Priority, ) => { const newTodo: Todo = { id: `todo-${Date.now()}`, title, completed: false, projectId, dueDate, priority, subtasks: [], createdAt: new Date(), updatedAt: new Date(), }; setTodos((prevTodos) => [...prevTodos, newTodo]); }; const handleUpdate = (updatedTodo: Todo) => { setTodos((prevTodos) => prevTodos.map((todo) => todo.id === updatedTodo.id ? { ...updatedTodo, updatedAt: new Date() } : todo, ), ); }; const handleDelete = (id: string) => { setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id)); }; const handleToggleStar = (id: string) => { setTodos((prevTodos) => prevTodos.map((todo) => todo.id === id ? { ...todo, starred: !todo.starred } : todo, ), ); }; const handleDragStart = (event: DragStartEvent) => { const { active } = event; const todo = todos.find((t) => t.id === active.id); setActiveTodo(todo || null); }; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id && sortBy === "manual") { const oldIndex = todos.findIndex((item) => item.id === active.id); const newIndex = todos.findIndex((item) => item.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { setTodos((items) => arrayMove(items, oldIndex, newIndex)); } } setActiveTodo(null); }; const hasTodos = todos.length > 0; return (
{!hasTodos ? ( {labels.emptyTitle} {labels.emptyDescription} ) : (

{labels.allTasks(filteredActiveTodos.length)}

t.id)} strategy={verticalListSortingStrategy} > {filteredActiveTodos.map((todo) => { const project = projects.find( (p) => p.id === todo.projectId, ); return ( ); })} {filteredCompletedTodos.length > 0 && (
{filteredCompletedTodos.map((todo) => { const project = projects.find( (p) => p.id === todo.projectId, ); return ( ); })}
)} {activeTodo ? (
{activeTodo.title}
) : null}
)}
); }; export { Todo10 };