Files

566 lines
17 KiB
TypeScript

"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 {
Check,
Edit,
GripVertical,
MoreVertical,
Plus,
Trash2,
X,
} from "lucide-react";
import { 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 { Input } from "@/components/ui/input";
import {
Item,
ItemActions,
ItemContent,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
import { cn } from "@/lib/utils";
interface Todo {
id: string;
title: string;
completed: boolean;
}
interface Todo5Props {
heading?: string;
description?: string;
addTaskLabel?: string;
taskPlaceholder?: string;
cancelLabel?: string;
submitLabel?: string;
editLabel?: string;
deleteLabel?: string;
saveEditLabel?: string;
cancelEditLabel?: string;
moreActionsLabel?: string;
editFieldLabel?: string;
deleteDialogTitle?: string;
deleteDialogDescription?: string;
deleteConfirmLabel?: string;
initialTodos?: Todo[];
className?: string;
}
const defaultTodos: Todo[] = [
{
id: "1",
title: "تکمیل مستندات فنی پروژه",
completed: false,
},
{
id: "2",
title: "بررسی و تأیید طراحی رابط کاربری",
completed: false,
},
{
id: "3",
title: "آماده‌سازی گزارش هفتگی",
completed: false,
},
];
interface SortableTodoItemProps {
todo: Todo;
onToggle: (id: string) => void;
editingTodo: string | null;
editTitle: string;
onStartEdit: (todo: Todo) => void;
onSaveEdit: () => void;
onCancelEdit: () => void;
onEditTitleChange: (title: string) => void;
onDeleteClick: (id: string) => void;
editLabel: string;
deleteLabel: string;
saveEditLabel: string;
cancelEditLabel: string;
moreActionsLabel: string;
editFieldLabel: string;
}
const SortableTodoItem = ({
todo,
onToggle,
editingTodo,
editTitle,
onStartEdit,
onSaveEdit,
onCancelEdit,
onEditTitleChange,
onDeleteClick,
editLabel,
deleteLabel,
saveEditLabel,
cancelEditLabel,
moreActionsLabel,
editFieldLabel,
}: 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,
};
return (
<div ref={setNodeRef} style={style}>
<Item
variant={todo.completed ? "muted" : "default"}
className={cn(
"group cursor-pointer items-center border-b transition-opacity",
todo.completed && "opacity-60",
)}
onClick={() => onToggle(todo.id)}
>
<ItemMedia>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon-sm"
{...attributes}
{...listeners}
className="cursor-grab active:cursor-grabbing"
onClick={(e) => e.stopPropagation()}
aria-label={`جابه‌جایی «${todo.title}»`}
>
<GripVertical className="size-4 text-muted-foreground" />
</Button>
<Checkbox
checked={todo.completed}
onCheckedChange={() => onToggle(todo.id)}
onClick={(e) => e.stopPropagation()}
aria-label={
todo.completed
? `علامت‌گذاری «${todo.title}» به‌عنوان انجام‌نشده`
: `علامت‌گذاری «${todo.title}» به‌عنوان انجام‌شده`
}
/>
</div>
</ItemMedia>
<ItemContent>
{editingTodo === todo.id ? (
<Input
value={editTitle}
onChange={(e) => onEditTitleChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
onSaveEdit();
}
if (e.key === "Escape") {
onCancelEdit();
}
}}
onClick={(e) => e.stopPropagation()}
className="text-base font-medium"
autoFocus
aria-label={editFieldLabel}
/>
) : (
<ItemTitle
className={
todo.completed ? "text-muted-foreground line-through" : ""
}
>
{todo.title}
</ItemTitle>
)}
</ItemContent>
<ItemActions>
{editingTodo === todo.id ? (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon-sm"
onClick={(e) => {
e.stopPropagation();
onSaveEdit();
}}
aria-label={saveEditLabel}
>
<Check className="h-4 w-4" aria-hidden />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={(e) => {
e.stopPropagation();
onCancelEdit();
}}
aria-label={cancelEditLabel}
>
<X className="h-4 w-4" aria-hidden />
</Button>
</div>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="opacity-0 transition-opacity group-hover:opacity-100"
onClick={(e) => e.stopPropagation()}
aria-label={moreActionsLabel}
>
<MoreVertical className="h-4 w-4" aria-hidden />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onStartEdit(todo);
}}
>
<Edit className="h-4 w-4" aria-hidden />
{editLabel}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={(e) => {
e.stopPropagation();
onDeleteClick(todo.id);
}}
>
<Trash2 className="h-4 w-4" aria-hidden />
{deleteLabel}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</ItemActions>
</Item>
</div>
);
};
const Todo5 = ({
heading = "مدیریت وظایف با ویرایش و حذف",
description =
"وظیفه جدید اضافه کنید، ترتیب را تغییر دهید، ویرایش کنید یا حذف کنید.",
addTaskLabel = "افزودن وظیفه جدید",
taskPlaceholder = "عنوان وظیفه",
cancelLabel = "انصراف",
submitLabel = "افزودن",
editLabel = "ویرایش",
deleteLabel = "حذف",
saveEditLabel = "ذخیره",
cancelEditLabel = "لغو",
moreActionsLabel = "گزینه‌های بیشتر",
editFieldLabel = "ویرایش عنوان وظیفه",
deleteDialogTitle = "حذف وظیفه؟",
deleteDialogDescription =
"این وظیفه برای همیشه حذف می‌شود و قابل بازگشت نیست.",
deleteConfirmLabel = "حذف",
initialTodos = defaultTodos,
className,
}: Todo5Props) => {
const [todos, setTodos] = useState(initialTodos);
const [isFormExpanded, setIsFormExpanded] = useState(false);
const [newTaskTitle, setNewTaskTitle] = useState("");
const [editingTodo, setEditingTodo] = useState<string | null>(null);
const [editTitle, setEditTitle] = useState("");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [todoToDelete, setTodoToDelete] = useState<string | null>(null);
const [activeTodo, setActiveTodo] = useState<Todo | null>(null);
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 handleAddTask = () => {
if (newTaskTitle.trim()) {
const newTodo: Todo = {
id: Date.now().toString(),
title: newTaskTitle.trim(),
completed: false,
};
setTodos((prevTodos) => [...prevTodos, newTodo]);
setNewTaskTitle("");
setIsFormExpanded(false);
}
};
const handleStartEdit = (todo: Todo) => {
setEditingTodo(todo.id);
setEditTitle(todo.title);
};
const handleSaveEdit = () => {
if (editingTodo && editTitle.trim()) {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === editingTodo ? { ...todo, title: editTitle.trim() } : todo,
),
);
setEditingTodo(null);
setEditTitle("");
}
};
const handleCancelEdit = () => {
setEditingTodo(null);
setEditTitle("");
};
const handleDeleteClick = (id: string) => {
setTodoToDelete(id);
setDeleteDialogOpen(true);
};
const handleDeleteConfirm = () => {
if (todoToDelete) {
setTodos((prevTodos) =>
prevTodos.filter((todo) => todo.id !== todoToDelete),
);
setDeleteDialogOpen(false);
setTodoToDelete(null);
}
};
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) {
setTodos((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
setActiveTodo(null);
};
return (
<section dir="rtl" lang="fa" className={cn("py-32", className)}>
<div className="container">
<div className="mx-auto flex max-w-2xl flex-col gap-8">
<div className="flex flex-col gap-2">
<h2 className="text-3xl font-semibold md:text-xl">{heading}</h2>
<p className="text-muted-foreground">{description}</p>
</div>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex max-w-2xl flex-col gap-4">
{!isFormExpanded ? (
<Button
variant="ghost"
size="lg"
className="w-full justify-start gap-2"
onClick={() => setIsFormExpanded(true)}
>
<Plus className="h-4 w-4" aria-hidden />
{addTaskLabel}
</Button>
) : (
<div className="flex flex-col gap-2 rounded-lg border p-4">
<Input
autoFocus
placeholder={taskPlaceholder}
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && newTaskTitle.trim()) {
handleAddTask();
}
if (e.key === "Escape") {
setNewTaskTitle("");
setIsFormExpanded(false);
}
}}
className="text-base font-medium"
aria-label={taskPlaceholder}
/>
<div className="flex items-center justify-end gap-2">
<Button
variant="secondary"
size="sm"
onClick={() => {
setNewTaskTitle("");
setIsFormExpanded(false);
}}
>
<X className="h-4 w-4" aria-hidden />
{cancelLabel}
</Button>
<Button
size="sm"
onClick={handleAddTask}
disabled={!newTaskTitle.trim()}
>
<Plus className="h-4 w-4" aria-hidden />
{submitLabel}
</Button>
</div>
</div>
)}
<SortableContext
items={todos.map((t) => t.id)}
strategy={verticalListSortingStrategy}
>
<ItemGroup>
{todos.map((todo) => (
<SortableTodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
editingTodo={editingTodo}
editTitle={editTitle}
onStartEdit={handleStartEdit}
onSaveEdit={handleSaveEdit}
onCancelEdit={handleCancelEdit}
onEditTitleChange={setEditTitle}
onDeleteClick={handleDeleteClick}
editLabel={editLabel}
deleteLabel={deleteLabel}
saveEditLabel={saveEditLabel}
cancelEditLabel={cancelEditLabel}
moreActionsLabel={moreActionsLabel}
editFieldLabel={editFieldLabel}
/>
))}
</ItemGroup>
</SortableContext>
</div>
<DragOverlay>
{activeTodo ? (
<Item
variant={activeTodo.completed ? "muted" : "default"}
className="cursor-grabbing items-center border-b opacity-90 shadow-lg"
>
<ItemMedia>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon-sm"
className="cursor-grabbing"
aria-hidden
tabIndex={-1}
>
<GripVertical className="size-4 text-muted-foreground" />
</Button>
<Checkbox
checked={activeTodo.completed}
aria-hidden
tabIndex={-1}
/>
</div>
</ItemMedia>
<ItemContent>
<ItemTitle
className={
activeTodo.completed
? "text-muted-foreground line-through"
: ""
}
>
{activeTodo.title}
</ItemTitle>
</ItemContent>
</Item>
) : null}
</DragOverlay>
</DndContext>
</div>
</div>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent dir="rtl" lang="fa" className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{deleteDialogTitle}</DialogTitle>
<DialogDescription>{deleteDialogDescription}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
>
{cancelLabel}
</Button>
<Button variant="destructive" onClick={handleDeleteConfirm}>
{deleteConfirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
);
};
export { Todo5 };