From 938a30cc44d5e68d079e37af8c88cabd1011dcce Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Sat, 6 Dec 2025 23:54:12 +0330 Subject: [PATCH 1/5] feat: add Jalali date picker and priority options to todo input, remove help modal --- src/components/date-picker/date-picker.tsx | 239 ++++++++++++++++++ src/components/date-picker/index.ts | 1 + .../priority-options/priority-options.tsx | 28 ++ .../widgets/todos/expandable-todo-input.tsx | 125 ++++----- src/layouts/widgets/todos/help-modal.tsx | 53 ---- src/layouts/widgets/todos/todos.tsx | 21 +- 6 files changed, 336 insertions(+), 131 deletions(-) create mode 100644 src/components/date-picker/date-picker.tsx create mode 100644 src/components/date-picker/index.ts create mode 100644 src/components/priority-options/priority-options.tsx delete mode 100644 src/layouts/widgets/todos/help-modal.tsx diff --git a/src/components/date-picker/date-picker.tsx b/src/components/date-picker/date-picker.tsx new file mode 100644 index 00000000..2d67a1eb --- /dev/null +++ b/src/components/date-picker/date-picker.tsx @@ -0,0 +1,239 @@ +import jalaliMoment from 'jalali-moment' +import { useState } from 'react' +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6' +import { TfiBackRight } from 'react-icons/tfi' + +interface DatePickerProps { + onDateSelect: (date: jalaliMoment.Moment) => void + selectedDate?: jalaliMoment.Moment + className?: string +} + +const WEEKDAYS = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج'] + +export function DatePicker({ + onDateSelect, + selectedDate, + className = '', +}: DatePickerProps) { + const [currentDate, setCurrentDate] = useState( + selectedDate?.locale('fa') || jalaliMoment().locale('fa') + ) + + const firstDayOfMonth = currentDate.clone().startOf('jMonth').day() + const daysInMonth = currentDate.clone().endOf('jMonth').jDate() + const emptyDays = (firstDayOfMonth + 1) % 7 + + const prevMonth = currentDate.clone().subtract(1, 'jMonth') + const daysInPrevMonth = prevMonth.clone().endOf('jMonth').jDate() + const prevMonthStartDay = daysInPrevMonth - emptyDays + 1 + + const totalCells = 42 + + const isToday = (date: jalaliMoment.Moment) => { + const today = jalaliMoment() + return ( + date.jDate() === today.jDate() && + date.jMonth() === today.jMonth() && + date.jYear() === today.jYear() + ) + } + + const isSelected = (date: jalaliMoment.Moment) => { + if (!selectedDate) return false + return ( + date.jDate() === selectedDate.jDate() && + date.jMonth() === selectedDate.jMonth() && + date.jYear() === selectedDate.jYear() + ) + } + + const isCurrentMonthToday = () => { + const today = jalaliMoment() + return ( + currentDate.jMonth() === today.jMonth() && + currentDate.jYear() === today.jYear() + ) + } + + const isTodaySelected = () => { + if (!selectedDate) return false + const today = jalaliMoment() + return ( + selectedDate.jDate() === today.jDate() && + selectedDate.jMonth() === today.jMonth() && + selectedDate.jYear() === today.jYear() + ) + } + + const changeMonth = (delta: number) => { + setCurrentDate((prev) => prev.clone().add(delta, 'jMonth')) + } + + const goToToday = () => { + const today = jalaliMoment().locale('fa') + setCurrentDate(today) + onDateSelect(today) + } + + const showTodayButton = !isCurrentMonthToday() || !isTodaySelected() + + const handleDateClick = ( + day: number, + isCurrentMonth: boolean = true, + isPrevMonth: boolean = false + ) => { + let targetDate: jalaliMoment.Moment + + if (isCurrentMonth) { + targetDate = currentDate.clone().jDate(day) + } else if (isPrevMonth) { + targetDate = currentDate.clone().subtract(1, 'jMonth').jDate(day) + } else { + targetDate = currentDate.clone().add(1, 'jMonth').jDate(day) + } + + onDateSelect(targetDate) + } + + const renderDay = ( + day: number, + isCurrentMonth: boolean = true, + isPrevMonth: boolean = false + ) => { + let cellDate: jalaliMoment.Moment + + if (isCurrentMonth) { + cellDate = currentDate.clone().jDate(day) + } else if (isPrevMonth) { + cellDate = currentDate.clone().subtract(1, 'jMonth').jDate(day) + } else { + cellDate = currentDate.clone().add(1, 'jMonth').jDate(day) + } + + const isDayToday = isToday(cellDate) + const isDaySelected = isSelected(cellDate) + const isFriday = cellDate.day() === 5 + + const getDayTextStyle = () => { + if (isDaySelected) { + return 'bg-primary text-white font-medium' + } + + if (isFriday) { + return 'text-error bg-error/10' + } + + if (!isCurrentMonth) { + return 'text-muted opacity-50' + } + + return 'text-content hover:bg-base-300' + } + + const getHoverStyle = () => { + if (isDaySelected) return '' + + if (isFriday) { + return 'hover:bg-red-400/10' + } + + return 'hover:bg-primary/10' + } + + const getTodayRingStyle = () => { + if (isDaySelected) return '' + + if (isFriday) { + return 'border border-dashed border-error/80' + } + + return 'border border-dashed border-primary/80' + } + + return ( +
handleDateClick(day, isCurrentMonth, isPrevMonth)} + className={` + relative p-0 rounded-2xl text-xs transition-all cursor-pointer + h-6 w-6 mx-auto flex items-center justify-center hover:scale-110 hover:shadow + ${getDayTextStyle()} + ${getHoverStyle()} + ${isDayToday ? `${getTodayRingStyle()} scale-110 shadow-lg` : ''} + `} + > + {day} +
+ ) + } + + const renderCalendarGrid = () => { + const cells = [] + + for (let i = 0; i < emptyDays; i++) { + cells.push(renderDay(prevMonthStartDay + i, false, true)) + } + + for (let day = 1; day <= daysInMonth; day++) { + cells.push(renderDay(day, true)) + } + + const remainingCells = totalCells - cells.length + for (let day = 1; day <= remainingCells; day++) { + cells.push(renderDay(day, false, false)) + } + + return cells + } + + return ( +
+
+

+ {currentDate.format('dddd، jD jMMMM jYYYY')} +

+
+ {showTodayButton && ( + + )} + + +
+
+ +
+ {WEEKDAYS.map((weekday, index) => ( +
+ {weekday} +
+ ))} +
+ +
{renderCalendarGrid()}
+
+ ) +} diff --git a/src/components/date-picker/index.ts b/src/components/date-picker/index.ts new file mode 100644 index 00000000..0396ad21 --- /dev/null +++ b/src/components/date-picker/index.ts @@ -0,0 +1 @@ +export { DatePicker as SimpleDatePicker } from './date-picker' diff --git a/src/components/priority-options/priority-options.tsx b/src/components/priority-options/priority-options.tsx new file mode 100644 index 00000000..569324eb --- /dev/null +++ b/src/components/priority-options/priority-options.tsx @@ -0,0 +1,28 @@ +import type { PRIORITY_OPTIONS } from '@/common/constant/priority_options' +import Tooltip from '../toolTip' +import { FiFlag } from 'react-icons/fi' + +export const PriorityButton = ({ + option, + isSelected, + onClick, +}: { + option: (typeof PRIORITY_OPTIONS)[0] + isSelected: boolean + onClick: () => void +}) => ( + + + +) diff --git a/src/layouts/widgets/todos/expandable-todo-input.tsx b/src/layouts/widgets/todos/expandable-todo-input.tsx index 3b0771eb..c792f6b1 100644 --- a/src/layouts/widgets/todos/expandable-todo-input.tsx +++ b/src/layouts/widgets/todos/expandable-todo-input.tsx @@ -1,69 +1,24 @@ import { AnimatePresence, motion } from 'framer-motion' -import { useEffect, useRef, useState, memo, useCallback } from 'react' -import { FiFlag, FiMessageSquare, FiPlus, FiTag } from 'react-icons/fi' +import { useEffect, useRef, useState, useCallback } from 'react' +import { FiMessageSquare, FiPlus, FiTag, FiCalendar } from 'react-icons/fi' import { Button } from '@/components/button/button' import { TextInput } from '@/components/text-input' -import Tooltip from '@/components/toolTip' import { type AddTodoInput, TodoPriority } from '@/context/todo.context' 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' interface ExpandableTodoInputProps { todoText: string onChangeTodoText: (value: string) => void - onAddTodo: (input: Omit) => void + onAddTodo: (input: Omit & { date?: string }) => void } -const PRIORITY_OPTIONS = [ - { - value: 'low', - ariaLabel: 'اولویت کم', - bgColor: 'bg-green-500', - hoverBgColor: 'hover:bg-green-600', - }, - { - value: 'medium', - ariaLabel: 'اولویت متوسط', - bgColor: 'bg-yellow-400', - hoverBgColor: 'hover:bg-yellow-400', - }, - { - value: 'high', - ariaLabel: 'اولویت زیاد', - bgColor: 'bg-red-500', - hoverBgColor: 'hover:bg-red-500', - }, -] - -const PriorityButton = memo( - ({ - option, - isSelected, - onClick, - }: { - option: (typeof PRIORITY_OPTIONS)[0] - isSelected: boolean - onClick: () => void - }) => ( - - - - ) -) - -PriorityButton.displayName = 'PriorityButton' - export function ExpandableTodoInput({ todoText, onChangeTodoText, @@ -73,8 +28,11 @@ export function ExpandableTodoInput({ const [priority, setPriority] = useState(TodoPriority.Medium) const [category, setCategory] = useState('') const [notes, setNotes] = useState('') + const [selectedDate, setSelectedDate] = useState() + const [isDatePickerOpen, setIsDatePickerOpen] = useState(false) const inputRef = useRef(null) const containerRef = useRef(null) + const datePickerButtonRef = useRef(null) const isAdding = useIsMutating({ mutationKey: ['addTodo'] }) > 0 useEffect(() => { @@ -84,7 +42,13 @@ export function ExpandableTodoInput({ !containerRef.current.contains(event.target as Node) && isExpanded ) { - if (!todoText.trim()) { + const isClickInsideDatePicker = + event.target instanceof Element && + (event.target.closest('[data-date-picker]') || + event.target.closest('.fixed') || + event.target.closest('[role="tooltip"]')) + + if (!todoText.trim() && !isClickInsideDatePicker) { setIsExpanded(false) } } @@ -105,6 +69,7 @@ export function ExpandableTodoInput({ setCategory('') setNotes('') setPriority(TodoPriority.Medium) + setSelectedDate(undefined) setIsExpanded(false) }, [onChangeTodoText]) @@ -115,10 +80,11 @@ export function ExpandableTodoInput({ category: category.trim() || undefined, notes: notes.trim() || undefined, priority: priority, + date: selectedDate ? formatDateStr(selectedDate) : undefined, }) resetForm() } - }, [todoText, category, notes, priority, onAddTodo, resetForm]) + }, [todoText, category, notes, priority, selectedDate, onAddTodo, resetForm]) const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -169,11 +135,48 @@ export function ExpandableTodoInput({ transition={{ duration: 0.2 }} >
-
- -
+
+
+
+ +
+
+ + { + setSelectedDate(date) + setIsDatePickerOpen(false) + }} + selectedDate={selectedDate} + /> + } + position="bottom-left" + contentClassName="!p-0 !bg-transparent !border-none !shadow-none" + /> +
+
+ +
{PRIORITY_OPTIONS.map((option) => ( void -} -export function TodoHelpModal({ show, onClose }: TodoHelpModalProps) { - useEffect(() => { - if (show) { - Analytics.event('view_todo_help_modal') - } - }, [show]) - - return ( - -
-
    -
  • - اضافه کردن وظیفه به روز خاص: برای اضافه کردن - وظیفه به یک روز خاص، ابتدا ویجت تقویم را فعال کنید، سپس روز مورد - نظر را انتخاب کنید و در نهایت وظیفه مورد نظر را وارد کنید. -
  • -
  • - حالت همه وظایف: با تغییر حالت به "همه وظایف"، - تمامی وظایف نمایش داده می‌شود. -
  • -
  • - حالت لیست ماهانه: با تغییر حالت به "لیست ماهانه"، - تمامی وظایف ماه جاری نمایش داده می‌شود. -
  • -
  • - حالت لیست روزانه: با تغییر حالت به "لیست روزانه"، - فقط وظایف امروز نمایش داده می‌شود. -
  • -
  • - انتخاب روز پیش‌فرض: در صورت عدم انتخاب روز از ویجت - تقویم، به طور پیش‌فرض روز امروز انتخاب می‌شود. -
  • -
  • - حفظ وظایف: برای جلوگیری از پاک شدن وظایف، بهتر - است وارد حساب کاربری خود شوید. -
  • -
-
-
- ) -} diff --git a/src/layouts/widgets/todos/todos.tsx b/src/layouts/widgets/todos/todos.tsx index b43058da..e287fda1 100644 --- a/src/layouts/widgets/todos/todos.tsx +++ b/src/layouts/widgets/todos/todos.tsx @@ -14,7 +14,6 @@ import { import { useState } from 'react' import { FaChartSimple } from 'react-icons/fa6' import { FiList } from 'react-icons/fi' -import { IoMdHelp } from 'react-icons/io' import { Button } from '@/components/button/button' import Tooltip from '@/components/toolTip' import { useDate } from '@/context/date.context' @@ -23,7 +22,6 @@ import { type AddTodoInput, TodoViewType, useTodoStore } from '@/context/todo.co import { formatDateStr } from '../calendar/utils' import { WidgetContainer } from '../widget-container' import { ExpandableTodoInput } from './expandable-todo-input' -import { TodoHelpModal } from './help-modal' import { SortableTodoItem } from './sortable-todo-item' import { TodoStats } from './todo-stats' import { useAuth } from '@/context/auth.context' @@ -40,7 +38,6 @@ export function TodosLayout({ onChangeTab }: Prop) { const { addTodo, todos, updateOptions, todoOptions, reorderTodos } = useTodoStore() const { blurMode } = useGeneralSetting() const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all') - const [showHelpModal, setShowHelpModal] = useState(false) const [showStats, setShowStats] = useState(false) const [todoText, setTodoText] = useState('') const selectedDateStr = formatDateStr(selectedDate.clone()) @@ -77,7 +74,7 @@ export function TodosLayout({ onChangeTab }: Prop) { selectedDateTodos = selectedDateTodos.filter((todo) => todo.completed) } - const handleAddTodo = (todoInput: Omit) => { + const handleAddTodo = (todoInput: Omit & { date?: string }) => { if (!isAuthenticated) { setShowAuthModal(true) return @@ -85,7 +82,7 @@ export function TodosLayout({ onChangeTab }: Prop) { addTodo({ ...todoInput, - date: selectedDateStr, + date: todoInput.date || selectedDateStr, }) setTodoText('') } @@ -130,19 +127,10 @@ export function TodosLayout({ onChangeTab }: Prop) { > یادداشت
- {isUpdating && }
-
- - - +
+ {isUpdating && }
From bf6afed98052eb312e1a8311def4a4e44f7e2217 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Sun, 7 Dec 2025 00:05:46 +0330 Subject: [PATCH 4/5] feat: enhance date picker button with error indicator in todo input --- src/layouts/widgets/todos/expandable-todo-input.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/layouts/widgets/todos/expandable-todo-input.tsx b/src/layouts/widgets/todos/expandable-todo-input.tsx index b5588b74..33f14ba0 100644 --- a/src/layouts/widgets/todos/expandable-todo-input.tsx +++ b/src/layouts/widgets/todos/expandable-todo-input.tsx @@ -142,11 +142,12 @@ export function ExpandableTodoInput({ >
-
+
onClickOpenDatePicker()} > + Date: Sun, 7 Dec 2025 00:10:13 +0330 Subject: [PATCH 5/5] feat: update todo filter functionality and add analytics tracking --- src/layouts/widgets/todos/todos.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/layouts/widgets/todos/todos.tsx b/src/layouts/widgets/todos/todos.tsx index e287fda1..f6092a84 100644 --- a/src/layouts/widgets/todos/todos.tsx +++ b/src/layouts/widgets/todos/todos.tsx @@ -28,6 +28,7 @@ import { useAuth } from '@/context/auth.context' import { AuthRequiredModal } from '@/components/auth/AuthRequiredModal' import { useIsMutating } from '@tanstack/react-query' import { IconLoading } from '@/components/loading/icon-loading' +import Analytics from '@/analytics' interface Prop { onChangeTab?: any @@ -37,7 +38,7 @@ export function TodosLayout({ onChangeTab }: Prop) { const { isAuthenticated } = useAuth() const { addTodo, todos, updateOptions, todoOptions, reorderTodos } = useTodoStore() const { blurMode } = useGeneralSetting() - const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all') + const [filter, setFilter] = useState<'active' | 'completed'>('active') const [showStats, setShowStats] = useState(false) const [todoText, setTodoText] = useState('') const selectedDateStr = formatDateStr(selectedDate.clone()) @@ -55,6 +56,11 @@ export function TodosLayout({ onChangeTab }: Prop) { updateOptions({ viewMode }) } + const updateTodoFilter = (newFilter: 'active' | 'completed') => { + setFilter(newFilter) + Analytics.event(`todo_filter_${newFilter}_click`) + } + let selectedDateTodos = todos.filter((todo) => todo.date === selectedDateStr) if (todoOptions.viewMode === TodoViewType.Monthly) { @@ -149,19 +155,13 @@ export function TodosLayout({ onChangeTab }: Prop) {
-