Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/context/todo.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ interface TodoContextType {
clearCompleted: (date?: string) => void
updateOptions: (options: Partial<TodoOptions>) => void
reorderTodos: (todos: Todo[]) => Promise<void>
isPending: boolean
}

const TodoContext = createContext<TodoContextType | null>(null)
Expand All @@ -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)
Expand Down Expand Up @@ -309,6 +310,7 @@ export function TodoProvider({ children }: { children: React.ReactNode }) {
todoOptions,
reorderTodos,
refetchTodos: refetch,
isPending: isPending || isAdding || isUpdating,
}}
>
{children}
Expand Down
4 changes: 2 additions & 2 deletions src/layouts/bookmark/components/modal/advanced.modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export function AdvancedModal({ title, onClose, isOpen, bookmark }: AdvancedModa
size="md"
onClick={() => onClose(null)}
className={
'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'
'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'
}
>
لغو
Expand All @@ -326,7 +326,7 @@ export function AdvancedModal({ title, onClose, isOpen, bookmark }: AdvancedModa
size="md"
isPrimary={true}
className={
'btn btn-circle !w-fit px-8 border-none shadow-none text-secondary rounded-xl transition-colors duration-300 ease-in-out'
'btn btn-circle !w-fit px-8 border-none shadow-none text-secondary !rounded-2xl transition-colors duration-300 ease-in-out'
}
>
ذخیره
Expand Down
4 changes: 2 additions & 2 deletions src/layouts/bookmark/components/modal/edit-bookmark.modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export function EditBookmarkModal({
size="md"
disabled={isUpdating}
className={
'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'
'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'
}
>
لغو
Expand All @@ -239,7 +239,7 @@ export function EditBookmarkModal({
isPrimary={true}
loading={isUpdating}
className={
'btn btn-circle !w-fit px-8 border-none shadow-none text-secondary rounded-xl transition-colors duration-300 ease-in-out'
'btn btn-circle !w-fit px-8 border-none shadow-none text-secondary !rounded-2xl transition-colors duration-300 ease-in-out'
}
>
ذخیره
Expand Down
216 changes: 216 additions & 0 deletions src/layouts/widgets/todos/edit-todo-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
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<jalaliMoment.Moment | undefined>(
todo.date ? parseTodoDate(todo.date) : undefined
)
const { mutateAsync, isPending } = useUpdateTodo()
const datePickerButtonRef = useRef<HTMLButtonElement>(null)

const [text, setText] = useState(todo.text)
const [notes, setNotes] = useState(todo.notes || '')
const [category, setCategory] = useState(todo.category || '')
const [priority, setPriority] = useState<TodoPriority>(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<HTMLTextAreaElement>) => {
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 (
<Modal
isOpen={isOpen}
onClose={onClose}
title="ویرایش وظیفه"
size="md"
direction="rtl"
>
<div className="p-1 space-y-2">
<div className="space-y-2">
<TextInput
value={text}
onChange={handleTextChange}
placeholder="متن وظیفه را وارد کنید"
className="text-sm"
debounce={false}
/>
</div>

<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="relative flex-shrink-0 text-center">
<span className="absolute w-2 h-2 rounded-full -left-0.5 -bottom-0.5 bg-error animate-pulse"></span>
<FiCalendar className="text-indigo-400" size={14} />
</div>
<button
ref={datePickerButtonRef}
onClick={() => setIsDatePickerOpen(!isDatePickerOpen)}
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"
>
{selectedDate
? selectedDate.locale('fa').format('dddd، jD jMMMM')
: 'انتخاب تاریخ'}
</button>
<ClickableTooltip
triggerRef={datePickerButtonRef}
isOpen={isDatePickerOpen}
setIsOpen={setIsDatePickerOpen}
content={
<DatePicker
onDateSelect={handleDateSelect}
selectedDate={selectedDate}
/>
}
contentClassName="!p-0 !bg-transparent !border-none !shadow-none"
/>
</div>
<div className="flex items-center gap-1">
{PRIORITY_OPTIONS.map((option) => (
<PriorityButton
key={option.value}
option={option}
isSelected={priority === option.value}
onClick={() =>
handlePriorityChange(option.value as TodoPriority)
}
/>
))}
</div>
</div>

<div className="flex items-center gap-3">
<div className="flex-shrink-0 text-center">
<FiTag className="text-indigo-400" size={14} />
</div>
<TextInput
value={category}
onChange={handleCategoryChange}
placeholder="دسته‌بندی (مثال: شخصی، کاری)"
className="text-xs placeholder:text-xs py-1.5"
debounce={false}
/>
</div>

<div className="flex items-center gap-2">
<div className="flex-shrink-0 text-center">
<FiMessageSquare className="text-indigo-400" size={14} />
</div>
<textarea
value={notes}
onChange={handleNotesChange}
placeholder="یادداشت یا لینک (اختیاری)"
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 "
/>
</div>

{/* Action Buttons */}
<div className="flex items-center justify-end gap-2 pt-4">
<Button
onClick={onClose}
size="md"
disabled={isPending}
className={
'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'
}
>
لغو
</Button>
<Button
onClick={handleSave}
disabled={!text?.trim() || isPending}
size="md"
isPrimary={true}
loading={isPending}
className={
'btn btn-circle !w-fit px-8 border-none shadow-none text-secondary !rounded-2xl transition-colors duration-300 ease-in-out'
}
>
ذخیره
</Button>
</div>
</div>
</Modal>
)
}
3 changes: 1 addition & 2 deletions src/layouts/widgets/todos/expandable-todo-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { useIsMutating } from '@tanstack/react-query'
import { IconLoading } from '@/components/loading/icon-loading'
import { ClickableTooltip } from '@/components/clickableTooltip'
import type jalaliMoment from 'jalali-moment'
import { formatDateStr } from '../calendar/utils'
import { DatePicker } from '@/components/date-picker/date-picker'
import { PRIORITY_OPTIONS } from '@/common/constant/priority_options'
import { PriorityButton } from '@/components/priority-options/priority-options'
Expand Down Expand Up @@ -81,7 +80,7 @@ export function ExpandableTodoInput({
category: category.trim() || undefined,
notes: notes.trim() || undefined,
priority: priority,
date: selectedDate ? formatDateStr(selectedDate) : undefined,
date: selectedDate?.add(3.5, 'hours').toISOString(),
})
resetForm()
}
Expand Down
33 changes: 31 additions & 2 deletions src/layouts/widgets/todos/todo.item.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type React from 'react'
import { useState } from 'react'
import { FiChevronDown, FiTrash2 } from 'react-icons/fi'
import { FiChevronDown, FiTrash2, FiEdit3 } from 'react-icons/fi'
import { MdDragIndicator } from 'react-icons/md'
import CustomCheckbox from '@/components/checkbox'
import { useTodoStore, type TodoPriority } from '@/context/todo.context'
Expand All @@ -15,6 +15,8 @@ import { translateError } from '@/utils/translate-error'
import { validate } from 'uuid'
import Analytics from '@/analytics'
import { IconLoading } from '@/components/loading/icon-loading'
import { parseTodoDate } from './tools/parse-date'
import { EditTodoModal } from './edit-todo-modal'

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

Expand All @@ -50,6 +53,13 @@ export function TodoItem({
setShowConfirmation(true)
}

const handleEdit = (e: React.MouseEvent) => {
e.stopPropagation()
if (!isAuthenticated)
return showToast('برای ویرایش وظیفه باید وارد شوید', 'error')
setShowEditModal(true)
}

const handleExpand = (e: React.MouseEvent) => {
e.stopPropagation()
setExpanded(!expanded)
Expand Down Expand Up @@ -179,6 +189,14 @@ export function TodoItem({

{/* Actions */}
<div className="flex items-center gap-x-1">
<button
onClick={handleEdit}
className={
'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'
}
>
<FiEdit3 size={13} />
</button>
<button
onClick={handleDelete}
className={
Expand Down Expand Up @@ -219,7 +237,11 @@ export function TodoItem({
>
{translatedPriority[todo.priority as TodoPriority]}
</span>
<span>{todo.date}</span>
<span className="flex-1 text-[10px] text-base-content/50">
{parseTodoDate(todo.date)
.locale('fa')
.format('ddd، jD jMMMM')}
</span>
</div>

{/* Notes */}
Expand All @@ -236,6 +258,13 @@ export function TodoItem({
variant="danger"
/>
)}
{showEditModal && (
<EditTodoModal
todo={todo}
isOpen={showEditModal}
onClose={() => setShowEditModal(false)}
/>
)}
</div>
)
}
Expand Down
Loading