"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 (
t.id)}
strategy={verticalListSortingStrategy}
>
{filteredTodos.length === 0 ? (
{searchQuery || selectedTags.length > 0
? emptySearchMessage
: emptyListMessage}
) : (
filteredTodos.map((todo) => (
))
)}
{activeTodo ? (
-
{activeTodo.title}
) : null}
);
};
export { Todo9 };