"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 { GripVertical, Plus, Search, Tag, X } from "lucide-react"; import { useMemo, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Item, ItemContent, ItemGroup, ItemMedia, ItemTitle, } from "@/components/ui/item"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; interface TagOption { id: string; label: string; } interface Todo { id: string; title: string; completed: boolean; tags: string[]; } interface Todo9Props { heading?: string; description?: string; searchPlaceholder?: string; filterByTagsLabel?: string; clearFiltersLabel?: string; emptySearchMessage?: string; emptyListMessage?: string; addTagLabel?: string; tagCatalog?: TagOption[]; initialTodos?: Todo[]; className?: string; } const defaultTagCatalog: TagOption[] = [ { id: "work", label: "کاری" }, { id: "personal", label: "شخصی" }, { id: "urgent", label: "فوری" }, { id: "shopping", label: "خرید" }, { id: "health", label: "سلامت" }, { id: "fitness", label: "ورزش" }, { id: "learning", label: "یادگیری" }, { id: "documentation", label: "مستندات" }, { id: "code-review", label: "بازبینی کد" }, ]; const defaultTodos: Todo[] = [ { id: "1", title: "تکمیل مستندات فنی پروژه", completed: false, tags: ["work", "urgent", "documentation"], }, { id: "2", title: "بررسی و تأیید طراحی رابط کاربری", completed: false, tags: ["work", "code-review"], }, { id: "3", title: "آماده‌سازی گزارش هفتگی", completed: false, tags: ["personal", "shopping"], }, { id: "4", title: "هماهنگی جلسات تیم", completed: true, tags: ["health", "fitness"], }, { id: "5", title: "بررسی مستندات فنی", completed: false, tags: ["learning", "work"], }, ]; interface SortableTodoItemProps { todo: Todo; onToggle: (id: string) => void; onRemoveTag: (id: string, tag: string) => void; onAddTag: (id: string, tag: string) => void; tagCatalog: TagOption[]; getTagLabel: (tagId: string) => string; addTagLabel: string; } const SortableTodoItem = ({ todo, onToggle, onRemoveTag, onAddTag, tagCatalog, getTagLabel, addTagLabel, }: SortableTodoItemProps) => { 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 availableToAdd = tagCatalog.filter((tag) => !todo.tags.includes(tag.id)); return (
onToggle(todo.id)} >
onToggle(todo.id)} onClick={(e) => e.stopPropagation()} aria-label={ todo.completed ? `علامت‌گذاری «${todo.title}» به‌عنوان انجام‌نشده` : `علامت‌گذاری «${todo.title}» به‌عنوان انجام‌شده` } />
{todo.title}
{todo.tags.map((tag) => ( { e.stopPropagation(); onRemoveTag(todo.id, tag); }} aria-label={`حذف برچسب ${getTagLabel(tag)}`} > {getTagLabel(tag)} ))}
{availableToAdd.map((tag) => ( ))}
); }; const Todo9 = ({ heading = "مدیریت وظایف با جستجو و برچسب", description = "جستجوی لحظه‌ای، چند برچسب برای هر وظیفه، فیلتر بر اساس برچسب و مرتب‌سازی با کشیدن. برچسب‌ها را به هر وظیفه اضافه یا حذف کنید.", searchPlaceholder = "جستجوی وظایف...", filterByTagsLabel = "فیلتر بر اساس برچسب:", clearFiltersLabel = "پاک کردن فیلترها", emptySearchMessage = "هیچ وظیفه‌ای با جستجوی شما مطابقت ندارد.", emptyListMessage = "وظیفه‌ای وجود ندارد.", addTagLabel = "افزودن برچسب", tagCatalog = defaultTagCatalog, initialTodos = defaultTodos, className, }: Todo9Props) => { const [todos, setTodos] = useState(initialTodos); const [searchQuery, setSearchQuery] = useState(""); const [selectedTags, setSelectedTags] = useState([]); const [activeTodo, setActiveTodo] = useState(null); const getTagLabel = (tagId: string) => tagCatalog.find((tag) => tag.id === tagId)?.label ?? tagId; const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const toggleTodo = (id: string) => { setTodos((prevTodos) => prevTodos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo, ), ); }; const removeTag = (id: string, tag: string) => { setTodos((prevTodos) => prevTodos.map((todo) => todo.id === id ? { ...todo, tags: todo.tags.filter((t) => t !== tag) } : todo, ), ); }; const addTag = (id: string, tag: string) => { setTodos((prevTodos) => prevTodos.map((todo) => todo.id === id && !todo.tags.includes(tag) ? { ...todo, tags: [...todo.tags, tag] } : todo, ), ); }; const toggleTagFilter = (tag: string) => { setSelectedTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag], ); }; const filteredTodos = useMemo(() => { const query = searchQuery.trim(); return todos.filter((todo) => { if (query && !todo.title.includes(query)) { return false; } if (selectedTags.length > 0) { const hasSelectedTag = selectedTags.some((tag) => todo.tags.includes(tag), ); if (!hasSelectedTag) return false; } return true; }); }, [todos, searchQuery, selectedTags]); const allTags = useMemo(() => { const tagSet = new Set(); todos.forEach((todo) => { todo.tags.forEach((tag) => tagSet.add(tag)); }); return Array.from(tagSet).sort((a, b) => getTagLabel(a).localeCompare(getTagLabel(b), "fa"), ); }, [todos, tagCatalog]); const handleDragStart = (event: DragStartEvent) => { const { active } = event; const todo = filteredTodos.find((t) => t.id === active.id); setActiveTodo(todo || null); }; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { const oldIndex = filteredTodos.findIndex((item) => item.id === active.id); const newIndex = filteredTodos.findIndex((item) => item.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { const reorderedFiltered = arrayMove(filteredTodos, oldIndex, newIndex); const reorderedIds = reorderedFiltered.map((t) => t.id); setTodos((items) => { const sortedItems = [...items].sort((a, b) => { const aIndex = reorderedIds.indexOf(a.id); const bIndex = reorderedIds.indexOf(b.id); if (aIndex === -1) return 1; if (bIndex === -1) return -1; return aIndex - bIndex; }); return sortedItems; }); } } setActiveTodo(null); }; return (

{heading}

{description}

setSearchQuery(e.target.value)} className="ps-9" aria-label={searchPlaceholder} />
{filterByTagsLabel} {allTags.map((tag) => ( toggleTagFilter(tag)} > {getTagLabel(tag)} ))} {selectedTags.length > 0 && ( )}
t.id)} strategy={verticalListSortingStrategy} > {filteredTodos.length === 0 ? (
{searchQuery || selectedTags.length > 0 ? emptySearchMessage : emptyListMessage}
) : ( filteredTodos.map((todo) => ( )) )}
{activeTodo ? (
{activeTodo.title}
) : null}
); }; export { Todo9 };