Files

370 lines
12 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 { ChevronDown, GripVertical } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Item,
ItemContent,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
import { cn } from "@/lib/utils";
interface Todo {
id: string;
title: string;
completed: boolean;
}
interface Todo3Props {
heading?: string;
description?: string;
activeLabel?: string;
completedLabel?: string;
initialTodos?: Todo[];
className?: string;
}
const defaultTodos: Todo[] = [
{
id: "1",
title: "تکمیل مستندات فنی پروژه",
completed: false,
},
{
id: "2",
title: "بررسی و تأیید طراحی رابط کاربری",
completed: false,
},
{
id: "3",
title: "آماده‌سازی گزارش هفتگی",
completed: false,
},
{
id: "4",
title: "بازبینی کد منابع",
completed: true,
},
{
id: "5",
title: "هماهنگی با تیم طراحی",
completed: false,
},
{
id: "6",
title: "به‌روزرسانی مستندات فنی",
completed: true,
},
];
interface SortableTodoItemProps {
todo: Todo;
onToggle: (id: string) => void;
isAnimating?: boolean;
}
const SortableTodoItem = ({
todo,
onToggle,
isAnimating = false,
}: 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 || isAnimating ? "muted" : "default"}
className={cn(
"cursor-pointer items-center border-b transition-all duration-500",
isAnimating && "opacity-0",
!isAnimating && 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 || isAnimating}
onCheckedChange={() => onToggle(todo.id)}
onClick={(e) => e.stopPropagation()}
aria-label={
todo.completed
? `علامت‌گذاری «${todo.title}» به‌عنوان انجام‌نشده`
: `علامت‌گذاری «${todo.title}» به‌عنوان انجام‌شده`
}
/>
</div>
</ItemMedia>
<ItemContent>
<ItemTitle
className={cn(
"transition-all duration-500",
(todo.completed || isAnimating) &&
"text-muted-foreground line-through",
)}
>
{todo.title}
</ItemTitle>
</ItemContent>
</Item>
</div>
);
};
const Todo3 = ({
heading = "مدیریت وظایف با بخش تکمیل‌شده‌ها",
description =
"ترتیب وظایف را با کشیدن تغییر دهید، وضعیت تکمیل را علامت بزنید و وظایف انجام‌شده را در بخش تاشو مشاهده کنید.",
activeLabel = "در انتظار",
completedLabel = "تکمیل‌شده",
initialTodos = defaultTodos,
className,
}: Todo3Props) => {
const [todos, setTodos] = useState(initialTodos);
const [isCompletedExpanded, setIsCompletedExpanded] = useState(true);
const [activeTodo, setActiveTodo] = useState<Todo | null>(null);
const [animatingTodoId, setAnimatingTodoId] = useState<string | null>(null);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const toggleTodo = (id: string) => {
const todo = todos.find((t) => t.id === id);
if (!todo) return;
if (!todo.completed) {
setAnimatingTodoId(id);
setTimeout(() => {
setTodos((prevTodos) =>
prevTodos.map((t) => (t.id === id ? { ...t, completed: true } : t)),
);
setAnimatingTodoId(null);
}, 1000);
} else {
setTodos((prevTodos) =>
prevTodos.map((t) => (t.id === id ? { ...t, completed: false } : t)),
);
}
};
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);
};
const activeTodos = todos.filter((todo) => !todo.completed);
const completedTodos = todos.filter((todo) => todo.completed);
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-6">
{activeTodos.length > 0 && (
<div className="flex flex-col gap-2">
<h3 className="text-lg font-semibold">
{activeLabel} ({activeTodos.length})
</h3>
<SortableContext
items={activeTodos.map((t) => t.id)}
strategy={verticalListSortingStrategy}
>
<ItemGroup>
{activeTodos.map((todo) => (
<SortableTodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
isAnimating={animatingTodoId === todo.id}
/>
))}
</ItemGroup>
</SortableContext>
</div>
)}
{completedTodos.length > 0 && (
<div className="flex flex-col gap-3 border-t border-border/50 pt-6">
<button
type="button"
onClick={() => setIsCompletedExpanded(!isCompletedExpanded)}
className="group flex w-full items-center gap-2 text-xs font-semibold text-muted-foreground transition-colors hover:text-foreground"
aria-expanded={isCompletedExpanded}
aria-label={
isCompletedExpanded
? `بستن بخش ${completedLabel}`
: `باز کردن بخش ${completedLabel}`
}
>
<ChevronDown
className={cn(
"size-3.5 text-muted-foreground transition-transform duration-200",
!isCompletedExpanded && "-rotate-90",
)}
aria-hidden
/>
<span>{completedLabel}</span>
<span className="rounded-full bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
{completedTodos.length}
</span>
</button>
<div
className={cn(
"overflow-hidden transition-all duration-300 ease-in-out",
isCompletedExpanded
? "max-h-[2000px] opacity-100"
: "max-h-0 opacity-0",
)}
>
<ItemGroup>
{completedTodos.map((todo) => (
<Item
key={todo.id}
variant="muted"
className="cursor-pointer items-center border-b opacity-60 transition-all duration-500 hover:opacity-80"
onClick={() => toggleTodo(todo.id)}
>
<ItemMedia>
<Checkbox
checked={todo.completed}
onCheckedChange={() => toggleTodo(todo.id)}
onClick={(e) => e.stopPropagation()}
aria-label={`علامت‌گذاری «${todo.title}» به‌عنوان انجام‌نشده`}
/>
</ItemMedia>
<ItemContent>
<ItemTitle className="text-muted-foreground line-through">
{todo.title}
</ItemTitle>
</ItemContent>
</Item>
))}
</ItemGroup>
</div>
</div>
)}
</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>
</section>
);
};
export { Todo3 };