From 800f46b395393e65f9ce9a838be1e826672420b7 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Sat, 20 Dec 2025 04:06:10 +0330 Subject: [PATCH 1/2] feat: add comprehensive todo edit functionality with modal - Add EditTodoModal component with date picker, priority selection, and notes editing - Integrate useUpdateTodo hook for proper API communication - Implement performance optimizations with useCallback and separate state management - Add Persian date formatting and RTL support - Update todo context with loading states - Add edit button to todo items with hover effects - Include proper error handling and validation - Add analytics tracking for edit operations --- src/context/todo.context.tsx | 8 +- src/layouts/widgets/todos/edit-todo-modal.tsx | 215 ++++++++++++++++++ .../widgets/todos/expandable-todo-input.tsx | 3 +- src/layouts/widgets/todos/todo.item.tsx | 33 ++- src/layouts/widgets/todos/todos.tsx | 9 +- src/layouts/widgets/todos/tools/parse-date.ts | 11 + 6 files changed, 266 insertions(+), 13 deletions(-) create mode 100644 src/layouts/widgets/todos/edit-todo-modal.tsx create mode 100644 src/layouts/widgets/todos/tools/parse-date.ts diff --git a/src/context/todo.context.tsx b/src/context/todo.context.tsx index 7d7416db..b5bd0b2f 100644 --- a/src/context/todo.context.tsx +++ b/src/context/todo.context.tsx @@ -46,6 +46,7 @@ interface TodoContextType { clearCompleted: (date?: string) => void updateOptions: (options: Partial) => void reorderTodos: (todos: Todo[]) => Promise + isPending: boolean } const TodoContext = createContext(null) @@ -57,9 +58,9 @@ export function TodoProvider({ children }: { children: React.ReactNode }) { viewMode: TodoViewType.All, }) - const { data: fetchedTodos, refetch } = useGetTodos(isAuthenticated) - const { mutateAsync: addTodoAsync } = useAddTodo() - const { mutateAsync: updateTodoAsync } = useUpdateTodo() + const { data: fetchedTodos, refetch, isPending } = useGetTodos(isAuthenticated) + const { mutateAsync: addTodoAsync, isPending: isAdding } = useAddTodo() + const { mutateAsync: updateTodoAsync, isPending: isUpdating } = useUpdateTodo() const { mutateAsync: reorderTodosAsync } = useReorderTodos() const didLoadInitialOptions = useRef(false) @@ -309,6 +310,7 @@ export function TodoProvider({ children }: { children: React.ReactNode }) { todoOptions, reorderTodos, refetchTodos: refetch, + isPending: isPending || isAdding || isUpdating, }} > {children} diff --git a/src/layouts/widgets/todos/edit-todo-modal.tsx b/src/layouts/widgets/todos/edit-todo-modal.tsx new file mode 100644 index 00000000..a89f1a39 --- /dev/null +++ b/src/layouts/widgets/todos/edit-todo-modal.tsx @@ -0,0 +1,215 @@ +import { useState, useRef, useEffect, useCallback } from 'react' +import { FiMessageSquare, FiTag, FiCalendar } from 'react-icons/fi' +import { TextInput } from '@/components/text-input' +import { Button } from '@/components/button/button' +import type { TodoPriority } from '@/context/todo.context' +import type { Todo } from '@/services/hooks/todo/todo.interface' +import Modal from '@/components/modal' +import { PRIORITY_OPTIONS } from '@/common/constant/priority_options' +import { PriorityButton } from '@/components/priority-options/priority-options' +import { ClickableTooltip } from '@/components/clickableTooltip' +import { DatePicker } from '@/components/date-picker/date-picker' +import type jalaliMoment from 'jalali-moment' +import { parseTodoDate } from './tools/parse-date' +import { showToast } from '@/common/toast' +import { useUpdateTodo } from '@/services/hooks/todo/update-todo.hook' +import { safeAwait } from '@/services/api' +import { translateError } from '@/utils/translate-error' +import { useQueryClient } from '@tanstack/react-query' +import Analytics from '@/analytics' + +interface EditTodoModalProps { + todo: Todo + isOpen: boolean + onClose: () => void +} + +export function EditTodoModal({ todo, isOpen, onClose }: EditTodoModalProps) { + const queryClient = useQueryClient() + + const [isDatePickerOpen, setIsDatePickerOpen] = useState(false) + const [selectedDate, setSelectedDate] = useState( + todo.date ? parseTodoDate(todo.date) : undefined + ) + const { mutateAsync, isPending } = useUpdateTodo() + const datePickerButtonRef = useRef(null) + + const [text, setText] = useState(todo.text) + const [notes, setNotes] = useState(todo.notes || '') + const [category, setCategory] = useState(todo.category || '') + const [priority, setPriority] = useState(todo.priority as TodoPriority) + + useEffect(() => { + setText(todo.text) + setNotes(todo.notes || '') + setCategory(todo.category || '') + setPriority(todo.priority as TodoPriority) + setSelectedDate(todo.date ? parseTodoDate(todo.date) : undefined) + Analytics.event('todo_edit_opened') + }, [todo]) + + const handleTextChange = useCallback((value: string) => { + setText(value) + }, []) + + const handleNotesChange = useCallback((e: React.ChangeEvent) => { + setNotes(e.target.value) + }, []) + + const handleCategoryChange = useCallback((value: string) => { + setCategory(value) + }, []) + + const handlePriorityChange = useCallback((newPriority: TodoPriority) => { + setPriority(newPriority) + }, []) + + const handleDateSelect = useCallback((date: jalaliMoment.Moment) => { + setSelectedDate(date) + setIsDatePickerOpen(false) + }, []) + + const handleSave = useCallback(async () => { + if (!text.trim()) { + showToast('متن وظیفه نمی‌تواند خالی باشد', 'error') + return + } + + const input = { + text, + notes, + category, + priority, + date: selectedDate?.add(3.5, 'hours').toISOString(), + } + + const [err, _] = await safeAwait( + mutateAsync({ + id: todo.onlineId || todo.id, + input, + }) + ) + + if (err) { + showToast(translateError(err) as any, 'error') + return + } + showToast('وظیفه با موفقیت ویرایش شد', 'success') + onClose() + queryClient.invalidateQueries({ queryKey: ['getTodos'] }) + }, [text, notes, category, priority, selectedDate, onClose]) + + return ( + +
+
+ +
+ +
+
+
+ + +
+ + + } + contentClassName="!p-0 !bg-transparent !border-none !shadow-none" + /> +
+
+ {PRIORITY_OPTIONS.map((option) => ( + + handlePriorityChange(option.value as TodoPriority) + } + /> + ))} +
+
+ +
+
+ +
+ +
+ + {/* Notes */} +
+
+ +
+