2026-07-11 21:21:29 +03:30
|
|
|
|
"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 { format } from "date-fns";
|
|
|
|
|
|
import { faIR } from "date-fns/locale";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Bell,
|
|
|
|
|
|
Calendar,
|
|
|
|
|
|
ChevronDown,
|
|
|
|
|
|
Filter,
|
|
|
|
|
|
Flag,
|
|
|
|
|
|
GripVertical,
|
|
|
|
|
|
Plus,
|
|
|
|
|
|
SendHorizonal,
|
|
|
|
|
|
SortAsc,
|
|
|
|
|
|
Star,
|
|
|
|
|
|
X,
|
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
|
import { useMemo, useState } from "react";
|
|
|
|
|
|
|
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
|
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
|
|
|
|
import {
|
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
|
DropdownMenuCheckboxItem,
|
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
|
DropdownMenuLabel,
|
|
|
|
|
|
DropdownMenuRadioGroup,
|
|
|
|
|
|
DropdownMenuRadioItem,
|
|
|
|
|
|
DropdownMenuSeparator,
|
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
|
|
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 { Separator } from "@/components/ui/separator";
|
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
|
|
|
|
|
|
type Priority = "P1" | "P2" | "P3" | "P4";
|
|
|
|
|
|
type SortOption = "manual" | "title" | "priority";
|
|
|
|
|
|
|
|
|
|
|
|
interface Project {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
color: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface PriorityOption {
|
|
|
|
|
|
value: Priority;
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
color: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Todo {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
completed: boolean;
|
|
|
|
|
|
priority?: Priority;
|
|
|
|
|
|
starred: boolean;
|
|
|
|
|
|
projectId: string;
|
|
|
|
|
|
dueDate?: Date;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Filters {
|
|
|
|
|
|
starredOnly: boolean;
|
|
|
|
|
|
priorities: Priority[];
|
|
|
|
|
|
projects: string[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Todo7Props {
|
|
|
|
|
|
heading?: string;
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
projects?: Project[];
|
|
|
|
|
|
priorities?: PriorityOption[];
|
|
|
|
|
|
filterLabel?: string;
|
|
|
|
|
|
filterByLabel?: string;
|
|
|
|
|
|
starredOnlyLabel?: string;
|
|
|
|
|
|
prioritySectionLabel?: string;
|
|
|
|
|
|
projectsSectionLabel?: string;
|
|
|
|
|
|
clearFiltersLabel?: string;
|
|
|
|
|
|
sortLabel?: string;
|
|
|
|
|
|
sortByLabel?: string;
|
|
|
|
|
|
manualOrderLabel?: string;
|
|
|
|
|
|
titleSortLabel?: string;
|
|
|
|
|
|
prioritySortLabel?: string;
|
|
|
|
|
|
addTaskLabel?: string;
|
|
|
|
|
|
taskPlaceholder?: string;
|
|
|
|
|
|
dateLabel?: string;
|
|
|
|
|
|
priorityLabel?: string;
|
|
|
|
|
|
remindersLabel?: string;
|
|
|
|
|
|
selectProjectLabel?: string;
|
|
|
|
|
|
cancelLabel?: string;
|
|
|
|
|
|
submitLabel?: string;
|
|
|
|
|
|
emptyFiltersMessage?: string;
|
|
|
|
|
|
taskCountLabel?: (count: number) => string;
|
|
|
|
|
|
sortBadges?: Record<SortOption, string>;
|
|
|
|
|
|
starredAriaLabel?: string;
|
|
|
|
|
|
initialTodos?: Todo[];
|
|
|
|
|
|
className?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const defaultProjects: Project[] = [
|
2026-07-15 20:22:14 +02:00
|
|
|
|
{ id: "personal", name: "کارهای شخصی", color: "#3b82f6" },
|
|
|
|
|
|
{ id: "work", name: "پروژههای کاری", color: "#8b5cf6" },
|
|
|
|
|
|
{ id: "shopping", name: "خرید", color: "#10b981" },
|
|
|
|
|
|
{ id: "fitness", name: "سلامت و تناسب اندام", color: "#f59e0b" },
|
|
|
|
|
|
{ id: "learning", name: "یادگیری", color: "#ec4899" },
|
2026-07-11 21:21:29 +03:30
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const defaultPriorities: PriorityOption[] = [
|
|
|
|
|
|
{ value: "P1", label: "P1 - فوری", color: "text-red-500" },
|
|
|
|
|
|
{ value: "P2", label: "P2 - بالا", color: "text-orange-500" },
|
|
|
|
|
|
{ value: "P3", label: "P3 - متوسط", color: "text-yellow-500" },
|
2026-07-15 20:22:14 +02:00
|
|
|
|
{ value: "P4", label: "P4 - کم", color: "text-blue-500" },
|
2026-07-11 21:21:29 +03:30
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const defaultSortBadges: Record<SortOption, string> = {
|
|
|
|
|
|
manual: "د",
|
|
|
|
|
|
title: "ع",
|
|
|
|
|
|
priority: "ا",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const defaultTodos: Todo[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "1",
|
2026-07-15 20:22:14 +02:00
|
|
|
|
title: "تکمیل مستندات فنی پروژه",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
completed: false,
|
|
|
|
|
|
priority: "P2",
|
|
|
|
|
|
starred: true,
|
|
|
|
|
|
projectId: "work",
|
|
|
|
|
|
dueDate: new Date(2025, 9, 25),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "2",
|
2026-07-15 20:22:14 +02:00
|
|
|
|
title: "بررسی و تأیید طراحی رابط کاربری",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
completed: false,
|
|
|
|
|
|
priority: "P3",
|
|
|
|
|
|
starred: false,
|
|
|
|
|
|
projectId: "work",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "3",
|
2026-07-15 20:22:14 +02:00
|
|
|
|
title: "آمادهسازی گزارش هفتگی",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
completed: false,
|
|
|
|
|
|
priority: "P4",
|
|
|
|
|
|
starred: false,
|
|
|
|
|
|
projectId: "shopping",
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const formatDueDate = (date: Date) =>
|
|
|
|
|
|
format(date, "d MMM", { locale: faIR });
|
|
|
|
|
|
|
|
|
|
|
|
interface SortableTodoItemProps {
|
|
|
|
|
|
todo: Todo;
|
|
|
|
|
|
onToggle: (id: string) => void;
|
|
|
|
|
|
projects: Project[];
|
|
|
|
|
|
priorities: PriorityOption[];
|
|
|
|
|
|
starredAriaLabel: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const SortableTodoItem = ({
|
|
|
|
|
|
todo,
|
|
|
|
|
|
onToggle,
|
|
|
|
|
|
projects,
|
|
|
|
|
|
priorities,
|
|
|
|
|
|
starredAriaLabel,
|
|
|
|
|
|
}: 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 priority = priorities.find((p) => p.value === todo.priority);
|
|
|
|
|
|
const project = projects.find((p) => p.id === todo.projectId);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div ref={setNodeRef} style={style}>
|
|
|
|
|
|
<Item
|
|
|
|
|
|
variant={todo.completed ? "muted" : "default"}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"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>
|
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
|
|
|
|
<ItemTitle
|
|
|
|
|
|
className={
|
|
|
|
|
|
todo.completed ? "text-muted-foreground line-through" : ""
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{todo.title}
|
|
|
|
|
|
</ItemTitle>
|
|
|
|
|
|
{todo.starred && (
|
|
|
|
|
|
<Star
|
|
|
|
|
|
className="size-4 fill-amber-500 text-amber-500"
|
|
|
|
|
|
aria-label={starredAriaLabel}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{priority && (
|
|
|
|
|
|
<Badge variant="outline" className={cn("text-xs", priority.color)}>
|
|
|
|
|
|
{priority.value}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{project && (
|
|
|
|
|
|
<Badge variant="outline" className="text-xs">
|
|
|
|
|
|
{project.name}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{todo.dueDate && (
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
|
{formatDueDate(todo.dueDate)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</ItemContent>
|
|
|
|
|
|
</Item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const Todo7 = ({
|
2026-07-15 20:22:14 +02:00
|
|
|
|
heading = "مدیریت پیشرفته وظایف",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
description =
|
2026-07-15 20:22:14 +02:00
|
|
|
|
"وظیفه با تاریخ، اولویت، یادآور و پروژه بسازید. ترتیب را تغییر دهید و فیلتر و مرتبسازی کنید.",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
projects = defaultProjects,
|
|
|
|
|
|
priorities = defaultPriorities,
|
|
|
|
|
|
filterLabel = "فیلتر",
|
|
|
|
|
|
filterByLabel = "فیلتر بر اساس",
|
2026-07-15 20:22:14 +02:00
|
|
|
|
starredOnlyLabel = "فقط علاقهمندیها",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
prioritySectionLabel = "اولویت",
|
|
|
|
|
|
projectsSectionLabel = "پروژهها",
|
|
|
|
|
|
clearFiltersLabel = "پاک کردن فیلترها",
|
|
|
|
|
|
sortLabel = "مرتبسازی",
|
|
|
|
|
|
sortByLabel = "مرتبسازی بر اساس",
|
|
|
|
|
|
manualOrderLabel = "ترتیب دستی",
|
|
|
|
|
|
titleSortLabel = "عنوان (الف–ی)",
|
|
|
|
|
|
prioritySortLabel = "اولویت",
|
2026-07-15 20:22:14 +02:00
|
|
|
|
addTaskLabel = "افزودن وظیفه جدید",
|
|
|
|
|
|
taskPlaceholder = "عنوان وظیفه",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
dateLabel = "تاریخ",
|
|
|
|
|
|
priorityLabel = "اولویت",
|
|
|
|
|
|
remindersLabel = "یادآورها",
|
|
|
|
|
|
selectProjectLabel = "انتخاب پروژه",
|
|
|
|
|
|
cancelLabel = "انصراف",
|
2026-07-15 20:22:14 +02:00
|
|
|
|
submitLabel = "افزودن",
|
|
|
|
|
|
emptyFiltersMessage = "هیچ وظیفهای با فیلترهای شما مطابقت ندارد.",
|
|
|
|
|
|
taskCountLabel = (count) => `${count} وظیفه`,
|
2026-07-11 21:21:29 +03:30
|
|
|
|
sortBadges = defaultSortBadges,
|
2026-07-15 20:22:14 +02:00
|
|
|
|
starredAriaLabel = "علاقهمندی",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
initialTodos = defaultTodos,
|
|
|
|
|
|
className,
|
|
|
|
|
|
}: Todo7Props) => {
|
|
|
|
|
|
const [todos, setTodos] = useState<Todo[]>(initialTodos);
|
|
|
|
|
|
const [isFormExpanded, setIsFormExpanded] = useState(false);
|
|
|
|
|
|
const [formData, setFormData] = useState({
|
|
|
|
|
|
title: "",
|
|
|
|
|
|
dueDate: undefined as Date | undefined,
|
|
|
|
|
|
priority: null as Priority | null,
|
|
|
|
|
|
reminders: [] as Date[],
|
|
|
|
|
|
projectId: projects[0]?.id ?? "personal",
|
|
|
|
|
|
});
|
|
|
|
|
|
const [datePickerOpen, setDatePickerOpen] = useState(false);
|
|
|
|
|
|
const [sortBy, setSortBy] = useState<SortOption>("manual");
|
|
|
|
|
|
const [filters, setFilters] = useState<Filters>({
|
|
|
|
|
|
starredOnly: false,
|
|
|
|
|
|
priorities: [],
|
|
|
|
|
|
projects: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
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 resetForm = () => ({
|
|
|
|
|
|
title: "",
|
|
|
|
|
|
dueDate: undefined,
|
|
|
|
|
|
priority: null as Priority | null,
|
|
|
|
|
|
reminders: [] as Date[],
|
|
|
|
|
|
projectId: projects[0]?.id ?? "personal",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const handleAddTask = () => {
|
|
|
|
|
|
if (formData.title.trim()) {
|
|
|
|
|
|
const newTodo: Todo = {
|
|
|
|
|
|
id: Date.now().toString(),
|
|
|
|
|
|
title: formData.title.trim(),
|
|
|
|
|
|
completed: false,
|
|
|
|
|
|
priority: formData.priority ?? undefined,
|
|
|
|
|
|
starred: false,
|
|
|
|
|
|
projectId: formData.projectId,
|
|
|
|
|
|
dueDate: formData.dueDate,
|
|
|
|
|
|
};
|
|
|
|
|
|
setTodos((prevTodos) => [...prevTodos, newTodo]);
|
|
|
|
|
|
setFormData(resetForm());
|
|
|
|
|
|
setIsFormExpanded(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
setFormData(resetForm());
|
|
|
|
|
|
setIsFormExpanded(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const togglePriorityFilter = (priority: Priority) => {
|
|
|
|
|
|
setFilters((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
priorities: prev.priorities.includes(priority)
|
|
|
|
|
|
? prev.priorities.filter((p) => p !== priority)
|
|
|
|
|
|
: [...prev.priorities, priority],
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const toggleProjectFilter = (projectId: string) => {
|
|
|
|
|
|
setFilters((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
projects: prev.projects.includes(projectId)
|
|
|
|
|
|
? prev.projects.filter((p) => p !== projectId)
|
|
|
|
|
|
: [...prev.projects, projectId],
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const clearFilters = () => {
|
|
|
|
|
|
setFilters({
|
|
|
|
|
|
starredOnly: false,
|
|
|
|
|
|
priorities: [],
|
|
|
|
|
|
projects: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const activeFilterCount =
|
|
|
|
|
|
(filters.starredOnly ? 1 : 0) +
|
|
|
|
|
|
filters.priorities.length +
|
|
|
|
|
|
filters.projects.length;
|
|
|
|
|
|
|
|
|
|
|
|
const filteredAndSortedTodos = useMemo(() => {
|
|
|
|
|
|
let filtered = todos.filter((todo) => {
|
|
|
|
|
|
if (filters.starredOnly && !todo.starred) return false;
|
|
|
|
|
|
if (
|
|
|
|
|
|
filters.priorities.length > 0 &&
|
|
|
|
|
|
todo.priority &&
|
|
|
|
|
|
!filters.priorities.includes(todo.priority)
|
|
|
|
|
|
)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
if (
|
|
|
|
|
|
filters.projects.length > 0 &&
|
|
|
|
|
|
!filters.projects.includes(todo.projectId)
|
|
|
|
|
|
)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (sortBy === "title") {
|
|
|
|
|
|
filtered = [...filtered].sort((a, b) =>
|
|
|
|
|
|
a.title.localeCompare(b.title, "fa"),
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (sortBy === "priority") {
|
|
|
|
|
|
const priorityOrder: Record<Priority, number> = {
|
|
|
|
|
|
P1: 1,
|
|
|
|
|
|
P2: 2,
|
|
|
|
|
|
P3: 3,
|
|
|
|
|
|
P4: 4,
|
|
|
|
|
|
};
|
|
|
|
|
|
filtered = [...filtered].sort(
|
|
|
|
|
|
(a, b) =>
|
|
|
|
|
|
(priorityOrder[(a.priority || "P4") as Priority] || 4) -
|
|
|
|
|
|
(priorityOrder[(b.priority || "P4") as Priority] || 4),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return filtered;
|
|
|
|
|
|
}, [todos, filters, sortBy]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleDragStart = (event: DragStartEvent) => {
|
|
|
|
|
|
const { active } = event;
|
|
|
|
|
|
const todo = filteredAndSortedTodos.find((t) => t.id === active.id);
|
|
|
|
|
|
setActiveTodo(todo || null);
|
|
|
|
|
|
if (sortBy !== "manual") {
|
|
|
|
|
|
setSortBy("manual");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
|
|
|
|
const { active, over } = event;
|
|
|
|
|
|
|
|
|
|
|
|
if (over && active.id !== over.id) {
|
|
|
|
|
|
const oldIndex = filteredAndSortedTodos.findIndex(
|
|
|
|
|
|
(item) => item.id === active.id,
|
|
|
|
|
|
);
|
|
|
|
|
|
const newIndex = filteredAndSortedTodos.findIndex(
|
|
|
|
|
|
(item) => item.id === over.id,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (oldIndex !== -1 && newIndex !== -1) {
|
|
|
|
|
|
const reorderedFiltered = arrayMove(
|
|
|
|
|
|
filteredAndSortedTodos,
|
|
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const selectedProject = projects.find((p) => p.id === formData.projectId);
|
|
|
|
|
|
const selectedPriority = priorities.find(
|
|
|
|
|
|
(p) => p.value === formData.priority,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
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">
|
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
|
<Button variant="outline" size="sm">
|
|
|
|
|
|
<Filter className="h-4 w-4" aria-hidden />
|
|
|
|
|
|
{filterLabel}
|
|
|
|
|
|
{activeFilterCount > 0 && (
|
|
|
|
|
|
<Badge variant="secondary" className="px-1.5 py-0">
|
|
|
|
|
|
{activeFilterCount}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
|
<DropdownMenuContent align="start" className="w-64">
|
|
|
|
|
|
<DropdownMenuLabel>{filterByLabel}</DropdownMenuLabel>
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
<DropdownMenuCheckboxItem
|
|
|
|
|
|
checked={filters.starredOnly}
|
|
|
|
|
|
onCheckedChange={(checked) =>
|
|
|
|
|
|
setFilters((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
starredOnly: checked,
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Star className="h-4 w-4" aria-hidden />
|
|
|
|
|
|
{starredOnlyLabel}
|
|
|
|
|
|
</DropdownMenuCheckboxItem>
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
<DropdownMenuLabel className="text-xs">
|
|
|
|
|
|
{prioritySectionLabel}
|
|
|
|
|
|
</DropdownMenuLabel>
|
|
|
|
|
|
{priorities.map((priority) => (
|
|
|
|
|
|
<DropdownMenuCheckboxItem
|
|
|
|
|
|
key={priority.value}
|
|
|
|
|
|
checked={filters.priorities.includes(priority.value)}
|
|
|
|
|
|
onCheckedChange={() =>
|
|
|
|
|
|
togglePriorityFilter(priority.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Flag
|
|
|
|
|
|
className={cn("h-4 w-4", priority.color)}
|
|
|
|
|
|
aria-hidden
|
|
|
|
|
|
/>
|
|
|
|
|
|
{priority.label}
|
|
|
|
|
|
</DropdownMenuCheckboxItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
<DropdownMenuLabel className="text-xs">
|
|
|
|
|
|
{projectsSectionLabel}
|
|
|
|
|
|
</DropdownMenuLabel>
|
|
|
|
|
|
{projects.map((project) => (
|
|
|
|
|
|
<DropdownMenuCheckboxItem
|
|
|
|
|
|
key={project.id}
|
|
|
|
|
|
checked={filters.projects.includes(project.id)}
|
|
|
|
|
|
onCheckedChange={() => toggleProjectFilter(project.id)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{project.name}
|
|
|
|
|
|
</DropdownMenuCheckboxItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{activeFilterCount > 0 && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="w-full"
|
|
|
|
|
|
onClick={clearFilters}
|
|
|
|
|
|
>
|
|
|
|
|
|
<X className="h-4 w-4" aria-hidden />
|
|
|
|
|
|
{clearFiltersLabel}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
|
|
|
|
|
|
|
<Separator orientation="vertical" className="h-8" />
|
|
|
|
|
|
|
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
|
<Button variant="outline" size="sm">
|
|
|
|
|
|
<SortAsc className="h-4 w-4" aria-hidden />
|
|
|
|
|
|
{sortLabel}
|
|
|
|
|
|
<Badge variant="secondary" className="px-1.5 py-0">
|
|
|
|
|
|
{sortBadges[sortBy]}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
|
<DropdownMenuContent align="start">
|
|
|
|
|
|
<DropdownMenuLabel>{sortByLabel}</DropdownMenuLabel>
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
<DropdownMenuRadioGroup
|
|
|
|
|
|
value={sortBy}
|
|
|
|
|
|
onValueChange={(value) => setSortBy(value as SortOption)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<DropdownMenuRadioItem value="manual">
|
|
|
|
|
|
{manualOrderLabel}
|
|
|
|
|
|
</DropdownMenuRadioItem>
|
|
|
|
|
|
<DropdownMenuRadioItem value="title">
|
|
|
|
|
|
{titleSortLabel}
|
|
|
|
|
|
</DropdownMenuRadioItem>
|
|
|
|
|
|
<DropdownMenuRadioItem value="priority">
|
|
|
|
|
|
{prioritySortLabel}
|
|
|
|
|
|
</DropdownMenuRadioItem>
|
|
|
|
|
|
</DropdownMenuRadioGroup>
|
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="ms-auto text-sm text-muted-foreground">
|
|
|
|
|
|
{taskCountLabel(filteredAndSortedTodos.length)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{!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>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<InputGroup className="flex-col">
|
|
|
|
|
|
<InputGroupInput
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
placeholder={taskPlaceholder}
|
|
|
|
|
|
value={formData.title}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
title: e.target.value,
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
|
if (e.key === "Enter" && formData.title.trim()) {
|
|
|
|
|
|
handleAddTask();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
|
handleCancel();
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="py-3 text-base font-medium"
|
|
|
|
|
|
aria-label={taskPlaceholder}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<InputGroupAddon
|
|
|
|
|
|
align="block-end"
|
|
|
|
|
|
className="flex-wrap border-t"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Popover
|
|
|
|
|
|
open={datePickerOpen}
|
|
|
|
|
|
onOpenChange={setDatePickerOpen}
|
|
|
|
|
|
>
|
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
|
<InputGroupButton
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className={formData.dueDate ? "text-chart-1" : ""}
|
|
|
|
|
|
aria-label={dateLabel}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Calendar className="h-4 w-4" aria-hidden />
|
|
|
|
|
|
{formData.dueDate ? (
|
|
|
|
|
|
<span className="text-xs">
|
|
|
|
|
|
{formatDueDate(formData.dueDate)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
dateLabel
|
|
|
|
|
|
)}
|
|
|
|
|
|
</InputGroupButton>
|
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
|
<PopoverContent
|
|
|
|
|
|
dir="rtl"
|
|
|
|
|
|
lang="fa"
|
|
|
|
|
|
align="start"
|
|
|
|
|
|
className="w-auto p-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<CalendarComponent
|
|
|
|
|
|
mode="single"
|
|
|
|
|
|
selected={formData.dueDate}
|
|
|
|
|
|
onSelect={(date) => {
|
|
|
|
|
|
setFormData((prev) => ({ ...prev, dueDate: date }));
|
|
|
|
|
|
if (date) {
|
|
|
|
|
|
setDatePickerOpen(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
disabled={(date) => date < new Date()}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
|
</Popover>
|
|
|
|
|
|
|
|
|
|
|
|
<Popover>
|
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
|
<InputGroupButton
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className={
|
|
|
|
|
|
selectedPriority ? selectedPriority.color : ""
|
|
|
|
|
|
}
|
|
|
|
|
|
aria-label={priorityLabel}
|
|
|
|
|
|
>
|
|
|
|
|
|
{selectedPriority ? (
|
|
|
|
|
|
<Flag
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"h-4 w-4",
|
|
|
|
|
|
selectedPriority.color,
|
|
|
|
|
|
)}
|
|
|
|
|
|
aria-hidden
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Flag className="h-4 w-4" aria-hidden />
|
|
|
|
|
|
)}
|
|
|
|
|
|
{selectedPriority?.label || priorityLabel}
|
|
|
|
|
|
</InputGroupButton>
|
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
|
<PopoverContent
|
|
|
|
|
|
dir="rtl"
|
|
|
|
|
|
lang="fa"
|
|
|
|
|
|
className="w-48 p-2"
|
|
|
|
|
|
align="start"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
{priorities.map((priority) => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
key={priority.value}
|
|
|
|
|
|
variant={
|
|
|
|
|
|
formData.priority === priority.value
|
|
|
|
|
|
? "secondary"
|
|
|
|
|
|
: "ghost"
|
|
|
|
|
|
}
|
|
|
|
|
|
className="w-full justify-start gap-2"
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
priority: priority.value,
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Flag
|
|
|
|
|
|
className={cn("h-3.5 w-3.5", priority.color)}
|
|
|
|
|
|
aria-hidden
|
|
|
|
|
|
/>
|
|
|
|
|
|
{priority.label}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
|
</Popover>
|
|
|
|
|
|
|
|
|
|
|
|
<InputGroupButton
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className={
|
|
|
|
|
|
formData.reminders.length > 0 ? "text-chart-2" : ""
|
|
|
|
|
|
}
|
|
|
|
|
|
aria-label={remindersLabel}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Bell className="h-4 w-4" aria-hidden />
|
|
|
|
|
|
{formData.reminders.length > 0
|
|
|
|
|
|
? `${formData.reminders.length}`
|
|
|
|
|
|
: remindersLabel}
|
|
|
|
|
|
</InputGroupButton>
|
|
|
|
|
|
</InputGroupAddon>
|
|
|
|
|
|
<InputGroupAddon align="block-end" className="border-t">
|
|
|
|
|
|
<Popover>
|
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
|
<InputGroupButton size="sm" className="gap-2">
|
|
|
|
|
|
{selectedProject
|
|
|
|
|
|
? selectedProject.name
|
|
|
|
|
|
: selectProjectLabel}
|
|
|
|
|
|
<ChevronDown className="h-3.5 w-3.5" aria-hidden />
|
|
|
|
|
|
</InputGroupButton>
|
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
|
<PopoverContent
|
|
|
|
|
|
dir="rtl"
|
|
|
|
|
|
lang="fa"
|
|
|
|
|
|
className="w-56 p-2"
|
|
|
|
|
|
align="start"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
{projects.map((project) => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
key={project.id}
|
|
|
|
|
|
variant={
|
|
|
|
|
|
formData.projectId === project.id
|
|
|
|
|
|
? "secondary"
|
|
|
|
|
|
: "ghost"
|
|
|
|
|
|
}
|
|
|
|
|
|
className="w-full justify-start"
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
projectId: project.id,
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{project.name}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
|
</Popover>
|
|
|
|
|
|
<InputGroupButton
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={handleCancel}
|
|
|
|
|
|
variant="secondary"
|
|
|
|
|
|
className="ms-auto"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X className="h-4 w-4" aria-hidden />
|
|
|
|
|
|
<span className="hidden sm:inline">{cancelLabel}</span>
|
|
|
|
|
|
</InputGroupButton>
|
|
|
|
|
|
<InputGroupButton
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={handleAddTask}
|
|
|
|
|
|
disabled={!formData.title.trim()}
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
>
|
|
|
|
|
|
<SendHorizonal
|
|
|
|
|
|
className="h-4 w-4 rtl:rotate-180"
|
|
|
|
|
|
aria-hidden
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="hidden sm:inline">{submitLabel}</span>
|
|
|
|
|
|
</InputGroupButton>
|
|
|
|
|
|
</InputGroupAddon>
|
|
|
|
|
|
</InputGroup>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<SortableContext
|
|
|
|
|
|
items={filteredAndSortedTodos.map((t) => t.id)}
|
|
|
|
|
|
strategy={verticalListSortingStrategy}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ItemGroup>
|
|
|
|
|
|
{filteredAndSortedTodos.length === 0 ? (
|
|
|
|
|
|
<div className="py-8 text-center text-sm text-muted-foreground">
|
|
|
|
|
|
{emptyFiltersMessage}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
filteredAndSortedTodos.map((todo) => (
|
|
|
|
|
|
<SortableTodoItem
|
|
|
|
|
|
key={todo.id}
|
|
|
|
|
|
todo={todo}
|
|
|
|
|
|
onToggle={toggleTodo}
|
|
|
|
|
|
projects={projects}
|
|
|
|
|
|
priorities={priorities}
|
|
|
|
|
|
starredAriaLabel={starredAriaLabel}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
</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>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<ItemTitle
|
|
|
|
|
|
className={
|
|
|
|
|
|
activeTodo.completed
|
|
|
|
|
|
? "text-muted-foreground line-through"
|
|
|
|
|
|
: ""
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{activeTodo.title}
|
|
|
|
|
|
</ItemTitle>
|
|
|
|
|
|
{activeTodo.starred && (
|
|
|
|
|
|
<Star
|
|
|
|
|
|
className="size-4 fill-amber-500 text-amber-500"
|
|
|
|
|
|
aria-hidden
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{activeTodo.priority && (
|
|
|
|
|
|
<Badge variant="outline" className="text-xs">
|
|
|
|
|
|
{activeTodo.priority}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</ItemContent>
|
|
|
|
|
|
</Item>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</DragOverlay>
|
|
|
|
|
|
</DndContext>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export { Todo7 };
|