From 646cd04144f60cd90ef2233d66c37fd206f921c3 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Sat, 27 Dec 2025 23:40:53 +0330 Subject: [PATCH 1/3] refactor: update TextInput and ExpandableTodoInput components for improved handling of controlled and uncontrolled states --- src/components/text-input.tsx | 62 ++++++-------- src/context/todo.context.tsx | 2 - .../widgets/todos/expandable-todo-input.tsx | 84 +++++++++++-------- src/layouts/widgets/todos/todos.tsx | 21 ++--- 4 files changed, 85 insertions(+), 84 deletions(-) diff --git a/src/components/text-input.tsx b/src/components/text-input.tsx index ca25176b..9cc61cdf 100644 --- a/src/components/text-input.tsx +++ b/src/components/text-input.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState, memo } from 'react' +import { useCallback, useEffect, useRef, memo } from 'react' enum TextInputSize { XS = 'xs', @@ -10,7 +10,8 @@ enum TextInputSize { interface TextInputProps { id?: string - value: string + value?: string + defaultValue?: string onChange: (value: string) => void placeholder?: string onFocus?: () => void @@ -41,6 +42,7 @@ const sizes: Record = { export const TextInput = memo(function TextInput({ onChange, value, + defaultValue, placeholder, onFocus, onKeyDown, @@ -59,27 +61,18 @@ export const TextInput = memo(function TextInput({ max, autoComplete = 'off', }: TextInputProps) { - const [localValue, setLocalValue] = useState(value) const debounceTimerRef = useRef(null) - - useEffect(() => { - if (debounce) { - setLocalValue(value) - } - }, [value, debounce]) + const isControlled = value !== undefined const handleChange = useCallback( (e: React.ChangeEvent) => { const newValue = e.target.value if (!debounce) { - // بدون هیچ تاخیری مستقیم onChange را صدا بزن onChange(newValue) return } - setLocalValue(newValue) - if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current) } @@ -102,30 +95,29 @@ export const TextInput = memo(function TextInput({ } }, []) - const displayValue = debounce ? localValue : value + const inputProps = { + ref, + id, + type, + name, + disabled, + onFocus, + onKeyDown, + dir: direction, + placeholder: placeholder || '', + className: `input bg-content w-full text-[14px] ${sizes[size]} rounded-xl !outline-none transition-all duration-300 focus:ring-1 focus:ring-blue-500/20 focus:border-primary font-light ${className}`, + onChange: handleChange, + maxLength, + autoComplete, + min, + max, + } - return ( - - ) + if (isControlled) { + return + } + + return }) TextInput.displayName = 'TextInput' diff --git a/src/context/todo.context.tsx b/src/context/todo.context.tsx index 5340c24b..04dbc6ea 100644 --- a/src/context/todo.context.tsx +++ b/src/context/todo.context.tsx @@ -14,7 +14,6 @@ import { useUpdateTodo } from '@/services/hooks/todo/update-todo.hook' import { useGetTodos } from '@/services/hooks/todo/get-todos.hook' import type { FetchedTodo, Todo } from '@/services/hooks/todo/todo.interface' import { playAlarm } from '@/common/playAlarm' -import { sleep } from '@/common/utils/timeout' export enum TodoViewType { Day = 'day', @@ -173,7 +172,6 @@ export function TodoProvider({ children }: { children: React.ReactNode }) { old.unshift({ ...item, order: 0, id }) setTodos(() => old) - await sleep(3000) const [err, _] = await safeAwait(addTodoAsync(item)) if (err) { const content = translateError(err) diff --git a/src/layouts/widgets/todos/expandable-todo-input.tsx b/src/layouts/widgets/todos/expandable-todo-input.tsx index 966c1241..d25e6528 100644 --- a/src/layouts/widgets/todos/expandable-todo-input.tsx +++ b/src/layouts/widgets/todos/expandable-todo-input.tsx @@ -15,38 +15,46 @@ import Analytics from '@/analytics' import { Chip } from '@/components/chip.component' import { useGetTags } from '@/services/hooks/todo/get-tags.hook' import { useAuth } from '@/context/auth.context' +import { useDate } from '@/context/date.context' interface ExpandableTodoInputProps { - todoText: string - onChangeTodoText: (value: string) => void - onAddTodo: (input: Omit & { date?: string }) => void + onAddTodo: (input: Omit & { date: string }) => void } -export function ExpandableTodoInput({ - todoText, - onChangeTodoText, - onAddTodo, -}: ExpandableTodoInputProps) { +export function ExpandableTodoInput({ onAddTodo }: ExpandableTodoInputProps) { const { isAuthenticated } = useAuth() + const { today } = useDate() const [isExpanded, setIsExpanded] = useState(false) const [priority, setPriority] = useState(TodoPriority.Medium) const [category, setCategory] = useState('') const { data: fetchedTags } = useGetTags(isAuthenticated) - const [notes, setNotes] = useState('') const [isTagTooltipOpen, setIsTagTooltipOpen] = useState(false) - const [selectedDate, setSelectedDate] = useState() + const [selectedDate, setSelectedDate] = useState(today) const [isDatePickerOpen, setIsDatePickerOpen] = useState(false) + const inputRef = useRef(null) + const todoTextRef = useRef('') + const notesRef = useRef('') const containerRef = useRef(null) const datePickerButtonRef = useRef(null) const categoryInputRef = useRef(null) + const notesInputRef = useRef(null) + const isAdding = useIsMutating({ mutationKey: ['addTodo'] }) > 0 - const onSelectCategory = (tag: string) => { + const handleTodoTextChange = useCallback((value: string) => { + todoTextRef.current = value + }, []) + + const onSelectCategory = useCallback((tag: string) => { setCategory(tag) setIsTagTooltipOpen(false) Analytics.event('todo_category_select') - } + }, []) + + const handleNotesChange = useCallback((value: string) => { + notesRef.current = value + }, []) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -61,7 +69,7 @@ export function ExpandableTodoInput({ event.target.closest('.fixed') || event.target.closest('[role="tooltip"]')) - if (!todoText.trim() && !isClickInsideDatePicker) { + if (!todoTextRef.current.trim() && !isClickInsideDatePicker) { setIsExpanded(false) } } @@ -71,47 +79,54 @@ export function ExpandableTodoInput({ return () => { document.removeEventListener('mousedown', handleClickOutside) } - }, [isExpanded, todoText]) + }, [isExpanded]) const handleInputFocus = useCallback(() => { setIsExpanded(true) }, []) const resetForm = useCallback(() => { - onChangeTodoText('') + todoTextRef.current = '' + notesRef.current = '' + if (inputRef.current) { + inputRef.current.value = '' + } + if (notesInputRef.current) { + notesInputRef.current.value = '' + } setCategory('') - setNotes('') setPriority(TodoPriority.Medium) - setSelectedDate(undefined) + setSelectedDate(today.clone()) setIsExpanded(false) - }, [onChangeTodoText]) + }, [today]) const handleAddTodo = useCallback(() => { - if (todoText.trim()) { + const text = todoTextRef.current.trim() + if (text) { onAddTodo({ - text: todoText.trim(), + text, category: category.trim() || undefined, - notes: notes.trim() || undefined, + notes: notesRef.current.trim() || undefined, priority: priority, - date: selectedDate?.add(3.5, 'hours').toISOString(), + date: selectedDate.toISOString(), }) resetForm() } - }, [todoText, category, notes, priority, selectedDate, onAddTodo, resetForm]) + }, [category, priority, selectedDate, onAddTodo, resetForm]) const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && todoText.trim()) { + if (e.key === 'Enter' && todoTextRef.current.trim()) { handleAddTodo() } }, - [todoText, handleAddTodo] + [handleAddTodo] ) - const onClickOpenDatePicker = () => { + const onClickOpenDatePicker = useCallback(() => { setIsDatePickerOpen(!isDatePickerOpen) Analytics.event('todo_datepicker_open_click') - } + }, [isDatePickerOpen]) return (
@@ -120,8 +135,8 @@ export function ExpandableTodoInput({
('all') const [showStats, setShowStats] = useState(false) - const [todoText, setTodoText] = useState('') const selectedDateStr = formatDateStr(selectedDate.clone()) const [showAuthModal, setShowAuthModal] = useState(false) @@ -90,7 +92,7 @@ export function TodosLayout({ onChangeTab }: Prop) { selectedDateTodos = selectedDateTodos.filter((todo) => todo.completed) } - const handleAddTodo = (todoInput: Omit & { date?: string }) => { + const handleAddTodo = (todoInput: Omit & { date: string }) => { if (!isAuthenticated) { setShowAuthModal(true) return @@ -98,9 +100,8 @@ export function TodosLayout({ onChangeTab }: Prop) { addTodo({ ...todoInput, - date: todoInput.date || selectedDateStr, + date: todoInput.date, }) - setTodoText('') } const handleDragEnd = (event: DragEndEvent) => { @@ -132,7 +133,7 @@ export function TodosLayout({ onChangeTab }: Prop) {
- وظایـف + وظایف
)} -
{' '} - {!showStats && ( - - )} +
+ {!showStats && }
Date: Sat, 27 Dec 2025 23:41:04 +0330 Subject: [PATCH 2/3] refactor: remove react-snowfall dependency and related component from WidgetifyLayout --- bun.lock | 5 ----- package.json | 1 - src/layouts/widgetify-card/widgetify.layout.tsx | 2 -- 3 files changed, 8 deletions(-) diff --git a/bun.lock b/bun.lock index d94683e9..5c10b801 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,6 @@ "react-hot-toast": "2.6.0", "react-icons": "5.5.0", "react-joyride": "2.9.3", - "react-snowfall": "^2.4.0", "swiper": "12.0.2", "tailwind-merge": "^3.4.0", "uuid": "13.0.0", @@ -1010,8 +1009,6 @@ "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], - "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], - "react-floater": ["react-floater@0.7.9", "", { "dependencies": { "deepmerge": "^4.3.1", "is-lite": "^0.8.2", "popper.js": "^1.16.0", "prop-types": "^15.8.1", "tree-changes": "^0.9.1" }, "peerDependencies": { "react": "15 - 18", "react-dom": "15 - 18" } }, "sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg=="], "react-ga4": ["react-ga4@2.1.0", "", {}, "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ=="], @@ -1028,8 +1025,6 @@ "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - "react-snowfall": ["react-snowfall@2.4.0", "", { "dependencies": { "react-fast-compare": "^3.2.2" }, "peerDependencies": { "react": "^16.8 || 17.x || 18.x || 19.x", "react-dom": "^16.8 || 17.x || 18.x || 19.x" } }, "sha512-KAPMiGnxt11PEgC2pTVrTQsvk5jt1kLUtG+ZamiKLphTZ7GiYT1Aa5kX6jp4jKWq1kqJHchnGT9CDm4g86A5Gg=="], - "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], diff --git a/package.json b/package.json index 6b09afd6..faab097f 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "react-hot-toast": "2.6.0", "react-icons": "5.5.0", "react-joyride": "2.9.3", - "react-snowfall": "^2.4.0", "swiper": "12.0.2", "tailwind-merge": "^3.4.0", "uuid": "13.0.0", diff --git a/src/layouts/widgetify-card/widgetify.layout.tsx b/src/layouts/widgetify-card/widgetify.layout.tsx index 85755940..3d5ef087 100644 --- a/src/layouts/widgetify-card/widgetify.layout.tsx +++ b/src/layouts/widgetify-card/widgetify.layout.tsx @@ -7,7 +7,6 @@ import { WidgetContainer } from '../widgets/widget-container' import { NotificationCenter } from './notification-center/notification-center' import { Pet } from './pets/pet' import { PetProvider } from './pets/pet.context' -import Snowfall from 'react-snowfall' export const WidgetifyLayout = () => { const { user, isAuthenticated } = useAuth() @@ -28,7 +27,6 @@ export const WidgetifyLayout = () => { return (
- { From da196dfcbb2d5219b724a0f9a2eac3cac54798a4 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Sun, 28 Dec 2025 01:15:57 +0330 Subject: [PATCH 3/3] refactor: enhance date filtering logic in TodosLayout for improved accuracy --- src/layouts/widgets/todos/todos.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/layouts/widgets/todos/todos.tsx b/src/layouts/widgets/todos/todos.tsx index 8907fe53..a5218f0d 100644 --- a/src/layouts/widgets/todos/todos.tsx +++ b/src/layouts/widgets/todos/todos.tsx @@ -29,6 +29,7 @@ import { SelectBox } from '@/components/selectbox/selectbox' import Analytics from '@/analytics' import { AuthRequiredModal } from '@/components/auth/AuthRequiredModal' import { IconLoading } from '@/components/loading/icon-loading' +import { parseTodoDate } from './tools/parse-date' const viewModeOptions = [ { value: TodoViewType.Day, label: 'لیست امروز' }, @@ -68,15 +69,19 @@ export function TodosLayout({ onChangeTab }: Prop) { Analytics.event(`todo_filter_${newFilter}_click`) } - let selectedDateTodos = todos.filter((todo) => todo.date === selectedDateStr) - + let selectedDateTodos = todos if (todoOptions.viewMode === TodoViewType.Monthly) { const currentMonth = selectedDate.format('jMM') + const currentYear = selectedDate.format('jYYYY') + selectedDateTodos = todos.filter((todo) => { + const todoDate = parseTodoDate(todo.date) + return todoDate.format('jYYYY-jMM') === `${currentYear}-${currentMonth}` + }) + } else if (todoOptions.viewMode === TodoViewType.Day) { selectedDateTodos = todos.filter((todo) => { - return todo.date.startsWith(`${selectedDate.year()}-${currentMonth}`) + const todoDate = parseTodoDate(todo.date) + return todoDate.format('jYYYY-jMM-jDD') === selectedDateStr }) - } else if (todoOptions.viewMode === TodoViewType.All) { - selectedDateTodos = todos } selectedDateTodos = selectedDateTodos.sort((a, b) => {