Skip to content

Commit 8bb1eb1

Browse files
authored
Merge pull request #361 from widgetify-app/feat/add-todo-edit-functionality
Feat/add todo edit functionality
2 parents 351c48f + 7ff5ba0 commit 8bb1eb1

8 files changed

Lines changed: 271 additions & 17 deletions

File tree

src/context/todo.context.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ interface TodoContextType {
4646
clearCompleted: (date?: string) => void
4747
updateOptions: (options: Partial<TodoOptions>) => void
4848
reorderTodos: (todos: Todo[]) => Promise<void>
49+
isPending: boolean
4950
}
5051

5152
const TodoContext = createContext<TodoContextType | null>(null)
@@ -57,9 +58,9 @@ export function TodoProvider({ children }: { children: React.ReactNode }) {
5758
viewMode: TodoViewType.All,
5859
})
5960

60-
const { data: fetchedTodos, refetch } = useGetTodos(isAuthenticated)
61-
const { mutateAsync: addTodoAsync } = useAddTodo()
62-
const { mutateAsync: updateTodoAsync } = useUpdateTodo()
61+
const { data: fetchedTodos, refetch, isPending } = useGetTodos(isAuthenticated)
62+
const { mutateAsync: addTodoAsync, isPending: isAdding } = useAddTodo()
63+
const { mutateAsync: updateTodoAsync, isPending: isUpdating } = useUpdateTodo()
6364
const { mutateAsync: reorderTodosAsync } = useReorderTodos()
6465

6566
const didLoadInitialOptions = useRef(false)
@@ -309,6 +310,7 @@ export function TodoProvider({ children }: { children: React.ReactNode }) {
309310
todoOptions,
310311
reorderTodos,
311312
refetchTodos: refetch,
313+
isPending: isPending || isAdding || isUpdating,
312314
}}
313315
>
314316
{children}

src/layouts/bookmark/components/modal/advanced.modal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export function AdvancedModal({ title, onClose, isOpen, bookmark }: AdvancedModa
315315
size="md"
316316
onClick={() => onClose(null)}
317317
className={
318-
'btn btn-circle !bg-base-300 hover:!bg-error/10 text-muted hover:!text-error px-10 border-none shadow-none rounded-xl transition-colors duration-300 ease-in-out'
318+
'btn btn-circle !bg-base-300 hover:!bg-error/10 text-muted hover:!text-error px-10 border-none shadow-none !rounded-2xl transition-colors duration-300 ease-in-out'
319319
}
320320
>
321321
لغو
@@ -326,7 +326,7 @@ export function AdvancedModal({ title, onClose, isOpen, bookmark }: AdvancedModa
326326
size="md"
327327
isPrimary={true}
328328
className={
329-
'btn btn-circle !w-fit px-8 border-none shadow-none text-secondary rounded-xl transition-colors duration-300 ease-in-out'
329+
'btn btn-circle !w-fit px-8 border-none shadow-none text-secondary !rounded-2xl transition-colors duration-300 ease-in-out'
330330
}
331331
>
332332
ذخیره

src/layouts/bookmark/components/modal/edit-bookmark.modal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export function EditBookmarkModal({
223223
size="md"
224224
disabled={isUpdating}
225225
className={
226-
'btn btn-circle !bg-base-300 hover:!bg-error/10 text-muted hover:!text-error px-10 border-none shadow-none rounded-xl transition-colors duration-300 ease-in-out'
226+
'btn btn-circle !bg-base-300 hover:!bg-error/10 text-muted hover:!text-error px-10 border-none shadow-none !rounded-2xl transition-colors duration-300 ease-in-out'
227227
}
228228
>
229229
لغو
@@ -239,7 +239,7 @@ export function EditBookmarkModal({
239239
isPrimary={true}
240240
loading={isUpdating}
241241
className={
242-
'btn btn-circle !w-fit px-8 border-none shadow-none text-secondary rounded-xl transition-colors duration-300 ease-in-out'
242+
'btn btn-circle !w-fit px-8 border-none shadow-none text-secondary !rounded-2xl transition-colors duration-300 ease-in-out'
243243
}
244244
>
245245
ذخیره
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { useState, useRef, useEffect, useCallback } from 'react'
2+
import { FiMessageSquare, FiTag, FiCalendar } from 'react-icons/fi'
3+
import { TextInput } from '@/components/text-input'
4+
import { Button } from '@/components/button/button'
5+
import type { TodoPriority } from '@/context/todo.context'
6+
import type { Todo } from '@/services/hooks/todo/todo.interface'
7+
import Modal from '@/components/modal'
8+
import { PRIORITY_OPTIONS } from '@/common/constant/priority_options'
9+
import { PriorityButton } from '@/components/priority-options/priority-options'
10+
import { ClickableTooltip } from '@/components/clickableTooltip'
11+
import { DatePicker } from '@/components/date-picker/date-picker'
12+
import type jalaliMoment from 'jalali-moment'
13+
import { parseTodoDate } from './tools/parse-date'
14+
import { showToast } from '@/common/toast'
15+
import { useUpdateTodo } from '@/services/hooks/todo/update-todo.hook'
16+
import { safeAwait } from '@/services/api'
17+
import { translateError } from '@/utils/translate-error'
18+
import { useQueryClient } from '@tanstack/react-query'
19+
import Analytics from '@/analytics'
20+
21+
interface EditTodoModalProps {
22+
todo: Todo
23+
isOpen: boolean
24+
onClose: () => void
25+
}
26+
27+
export function EditTodoModal({ todo, isOpen, onClose }: EditTodoModalProps) {
28+
const queryClient = useQueryClient()
29+
30+
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false)
31+
const [selectedDate, setSelectedDate] = useState<jalaliMoment.Moment | undefined>(
32+
todo.date ? parseTodoDate(todo.date) : undefined
33+
)
34+
const { mutateAsync, isPending } = useUpdateTodo()
35+
const datePickerButtonRef = useRef<HTMLButtonElement>(null)
36+
37+
const [text, setText] = useState(todo.text)
38+
const [notes, setNotes] = useState(todo.notes || '')
39+
const [category, setCategory] = useState(todo.category || '')
40+
const [priority, setPriority] = useState<TodoPriority>(todo.priority as TodoPriority)
41+
42+
useEffect(() => {
43+
setText(todo.text)
44+
setNotes(todo.notes || '')
45+
setCategory(todo.category || '')
46+
setPriority(todo.priority as TodoPriority)
47+
setSelectedDate(todo.date ? parseTodoDate(todo.date) : undefined)
48+
Analytics.event('todo_edit_opened')
49+
}, [todo])
50+
51+
const handleTextChange = useCallback((value: string) => {
52+
setText(value)
53+
}, [])
54+
55+
const handleNotesChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
56+
setNotes(e.target.value)
57+
}, [])
58+
59+
const handleCategoryChange = useCallback((value: string) => {
60+
setCategory(value)
61+
}, [])
62+
63+
const handlePriorityChange = useCallback((newPriority: TodoPriority) => {
64+
setPriority(newPriority)
65+
}, [])
66+
67+
const handleDateSelect = useCallback((date: jalaliMoment.Moment) => {
68+
setSelectedDate(date)
69+
setIsDatePickerOpen(false)
70+
}, [])
71+
72+
const handleSave = useCallback(async () => {
73+
if (!text.trim()) {
74+
showToast('متن وظیفه نمی‌تواند خالی باشد', 'error')
75+
return
76+
}
77+
78+
const input = {
79+
text,
80+
notes,
81+
category,
82+
priority,
83+
date: selectedDate?.add(3.5, 'hours').toISOString(),
84+
}
85+
86+
const [err, _] = await safeAwait(
87+
mutateAsync({
88+
id: todo.onlineId || todo.id,
89+
input,
90+
})
91+
)
92+
93+
if (err) {
94+
showToast(translateError(err) as any, 'error')
95+
return
96+
}
97+
showToast('وظیفه با موفقیت ویرایش شد', 'success')
98+
onClose()
99+
queryClient.invalidateQueries({ queryKey: ['getTodos'] })
100+
}, [text, notes, category, priority, selectedDate, onClose])
101+
102+
return (
103+
<Modal
104+
isOpen={isOpen}
105+
onClose={onClose}
106+
title="ویرایش وظیفه"
107+
size="md"
108+
direction="rtl"
109+
>
110+
<div className="p-1 space-y-2">
111+
<div className="space-y-2">
112+
<TextInput
113+
value={text}
114+
onChange={handleTextChange}
115+
placeholder="متن وظیفه را وارد کنید"
116+
className="text-sm"
117+
debounce={false}
118+
/>
119+
</div>
120+
121+
<div className="flex items-center justify-between">
122+
<div className="flex items-center gap-3">
123+
<div className="relative flex-shrink-0 text-center">
124+
<span className="absolute w-2 h-2 rounded-full -left-0.5 -bottom-0.5 bg-error animate-pulse"></span>
125+
<FiCalendar className="text-indigo-400" size={14} />
126+
</div>
127+
<button
128+
ref={datePickerButtonRef}
129+
onClick={() => setIsDatePickerOpen(!isDatePickerOpen)}
130+
className="min-w-full text-right px-2 py-1.5 min-h-8 text-xs rounded-xl border border-base-300 hover:border-primary/50 transition-colors bg-content text-content opacity-75 cursor-pointer"
131+
>
132+
{selectedDate
133+
? selectedDate.locale('fa').format('dddd، jD jMMMM')
134+
: 'انتخاب تاریخ'}
135+
</button>
136+
<ClickableTooltip
137+
triggerRef={datePickerButtonRef}
138+
isOpen={isDatePickerOpen}
139+
setIsOpen={setIsDatePickerOpen}
140+
content={
141+
<DatePicker
142+
onDateSelect={handleDateSelect}
143+
selectedDate={selectedDate}
144+
/>
145+
}
146+
contentClassName="!p-0 !bg-transparent !border-none !shadow-none"
147+
/>
148+
</div>
149+
<div className="flex items-center gap-1">
150+
{PRIORITY_OPTIONS.map((option) => (
151+
<PriorityButton
152+
key={option.value}
153+
option={option}
154+
isSelected={priority === option.value}
155+
onClick={() =>
156+
handlePriorityChange(option.value as TodoPriority)
157+
}
158+
/>
159+
))}
160+
</div>
161+
</div>
162+
163+
<div className="flex items-center gap-3">
164+
<div className="flex-shrink-0 text-center">
165+
<FiTag className="text-indigo-400" size={14} />
166+
</div>
167+
<TextInput
168+
value={category}
169+
onChange={handleCategoryChange}
170+
placeholder="دسته‌بندی (مثال: شخصی، کاری)"
171+
className="text-xs placeholder:text-xs py-1.5"
172+
debounce={false}
173+
/>
174+
</div>
175+
176+
<div className="flex items-center gap-2">
177+
<div className="flex-shrink-0 text-center">
178+
<FiMessageSquare className="text-indigo-400" size={14} />
179+
</div>
180+
<textarea
181+
value={notes}
182+
onChange={handleNotesChange}
183+
placeholder="یادداشت یا لینک (اختیاری)"
184+
className="w-full px-4 py-2 mt-2 text-base font-light leading-relaxed transition-all border-none outline-none resize-none bg-content min-h-48 focus:ring-1 focus:ring-primary/30 rounded-xl text-muted "
185+
/>
186+
</div>
187+
188+
{/* Action Buttons */}
189+
<div className="flex items-center justify-end gap-2 pt-4">
190+
<Button
191+
onClick={onClose}
192+
size="md"
193+
disabled={isPending}
194+
className={
195+
'btn btn-circle !bg-base-300 hover:!bg-error/10 text-muted hover:!text-error px-10 border-none shadow-none !rounded-2xl transition-colors duration-300 ease-in-out'
196+
}
197+
>
198+
لغو
199+
</Button>
200+
<Button
201+
onClick={handleSave}
202+
disabled={!text?.trim() || isPending}
203+
size="md"
204+
isPrimary={true}
205+
loading={isPending}
206+
className={
207+
'btn btn-circle !w-fit px-8 border-none shadow-none text-secondary !rounded-2xl transition-colors duration-300 ease-in-out'
208+
}
209+
>
210+
ذخیره
211+
</Button>
212+
</div>
213+
</div>
214+
</Modal>
215+
)
216+
}

src/layouts/widgets/todos/expandable-todo-input.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { useIsMutating } from '@tanstack/react-query'
88
import { IconLoading } from '@/components/loading/icon-loading'
99
import { ClickableTooltip } from '@/components/clickableTooltip'
1010
import type jalaliMoment from 'jalali-moment'
11-
import { formatDateStr } from '../calendar/utils'
1211
import { DatePicker } from '@/components/date-picker/date-picker'
1312
import { PRIORITY_OPTIONS } from '@/common/constant/priority_options'
1413
import { PriorityButton } from '@/components/priority-options/priority-options'
@@ -81,7 +80,7 @@ export function ExpandableTodoInput({
8180
category: category.trim() || undefined,
8281
notes: notes.trim() || undefined,
8382
priority: priority,
84-
date: selectedDate ? formatDateStr(selectedDate) : undefined,
83+
date: selectedDate?.add(3.5, 'hours').toISOString(),
8584
})
8685
resetForm()
8786
}

src/layouts/widgets/todos/todo.item.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type React from 'react'
22
import { useState } from 'react'
3-
import { FiChevronDown, FiTrash2 } from 'react-icons/fi'
3+
import { FiChevronDown, FiTrash2, FiEdit3 } from 'react-icons/fi'
44
import { MdDragIndicator } from 'react-icons/md'
55
import CustomCheckbox from '@/components/checkbox'
66
import { useTodoStore, type TodoPriority } from '@/context/todo.context'
@@ -15,6 +15,8 @@ import { translateError } from '@/utils/translate-error'
1515
import { validate } from 'uuid'
1616
import Analytics from '@/analytics'
1717
import { IconLoading } from '@/components/loading/icon-loading'
18+
import { parseTodoDate } from './tools/parse-date'
19+
import { EditTodoModal } from './edit-todo-modal'
1820

1921
interface Prop {
2022
todo: Todo
@@ -39,6 +41,7 @@ export function TodoItem({
3941
const { isAuthenticated } = useAuth()
4042
const [expanded, setExpanded] = useState(false)
4143
const [showConfirmation, setShowConfirmation] = useState(false)
44+
const [showEditModal, setShowEditModal] = useState(false)
4245
const isUpdating = useIsMutating({ mutationKey: ['updateTodo'] }) > 0
4346
const { mutateAsync, isPending } = useRemoveTodo(todo.onlineId || todo.id)
4447

@@ -50,6 +53,13 @@ export function TodoItem({
5053
setShowConfirmation(true)
5154
}
5255

56+
const handleEdit = (e: React.MouseEvent) => {
57+
e.stopPropagation()
58+
if (!isAuthenticated)
59+
return showToast('برای ویرایش وظیفه باید وارد شوید', 'error')
60+
setShowEditModal(true)
61+
}
62+
5363
const handleExpand = (e: React.MouseEvent) => {
5464
e.stopPropagation()
5565
setExpanded(!expanded)
@@ -179,6 +189,14 @@ export function TodoItem({
179189

180190
{/* Actions */}
181191
<div className="flex items-center gap-x-1">
192+
<button
193+
onClick={handleEdit}
194+
className={
195+
'p-1 rounded-full cursor-pointer hover:bg-blue-500/10 opacity-0 group-hover:opacity-100 transition-all duration-200 delay-100 group-hover:select-none'
196+
}
197+
>
198+
<FiEdit3 size={13} />
199+
</button>
182200
<button
183201
onClick={handleDelete}
184202
className={
@@ -219,7 +237,11 @@ export function TodoItem({
219237
>
220238
{translatedPriority[todo.priority as TodoPriority]}
221239
</span>
222-
<span>{todo.date}</span>
240+
<span className="flex-1 text-[10px] text-base-content/50">
241+
{parseTodoDate(todo.date)
242+
.locale('fa')
243+
.format('ddd، jD jMMMM')}
244+
</span>
223245
</div>
224246

225247
{/* Notes */}
@@ -236,6 +258,13 @@ export function TodoItem({
236258
variant="danger"
237259
/>
238260
)}
261+
{showEditModal && (
262+
<EditTodoModal
263+
todo={todo}
264+
isOpen={showEditModal}
265+
onClose={() => setShowEditModal(false)}
266+
/>
267+
)}
239268
</div>
240269
)
241270
}

0 commit comments

Comments
 (0)