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
5 changes: 0 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
62 changes: 27 additions & 35 deletions src/components/text-input.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState, memo } from 'react'
import { useCallback, useEffect, useRef, memo } from 'react'

enum TextInputSize {
XS = 'xs',
Expand All @@ -10,7 +10,8 @@ enum TextInputSize {

interface TextInputProps {
id?: string
value: string
value?: string
defaultValue?: string
onChange: (value: string) => void
placeholder?: string
onFocus?: () => void
Expand Down Expand Up @@ -41,6 +42,7 @@ const sizes: Record<TextInputSize, string> = {
export const TextInput = memo(function TextInput({
onChange,
value,
defaultValue,
placeholder,
onFocus,
onKeyDown,
Expand All @@ -59,27 +61,18 @@ export const TextInput = memo(function TextInput({
max,
autoComplete = 'off',
}: TextInputProps) {
const [localValue, setLocalValue] = useState(value)
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)

useEffect(() => {
if (debounce) {
setLocalValue(value)
}
}, [value, debounce])
const isControlled = value !== undefined

const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value

if (!debounce) {
// بدون هیچ تاخیری مستقیم onChange را صدا بزن
onChange(newValue)
return
}

setLocalValue(newValue)

if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
Expand All @@ -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 (
<input
ref={ref}
id={id}
type={type}
name={name}
value={displayValue}
disabled={disabled}
onFocus={onFocus}
onKeyDown={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={maxLength}
autoComplete={autoComplete}
min={min}
max={max}
/>
)
if (isControlled) {
return <input {...inputProps} value={value} />
}

return <input {...inputProps} defaultValue={defaultValue} />
})

TextInput.displayName = 'TextInput'
2 changes: 0 additions & 2 deletions src/context/todo.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 0 additions & 2 deletions src/layouts/widgetify-card/widgetify.layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -28,7 +27,6 @@ export const WidgetifyLayout = () => {
return (
<WidgetContainer className="overflow-hidden !h-72 !min-h-72 !max-h-72">
<div className="relative w-full h-full">
<Snowfall snowflakeCount={20} speed={[0.1, 0.3]} />
{
<PetProvider>
<Pet />
Expand Down
84 changes: 50 additions & 34 deletions src/layouts/widgets/todos/expandable-todo-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AddTodoInput, 'date'> & { date?: string }) => void
onAddTodo: (input: Omit<AddTodoInput, 'date'> & { 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>(TodoPriority.Medium)
const [category, setCategory] = useState('')
const { data: fetchedTags } = useGetTags(isAuthenticated)
const [notes, setNotes] = useState('')
const [isTagTooltipOpen, setIsTagTooltipOpen] = useState(false)
const [selectedDate, setSelectedDate] = useState<jalaliMoment.Moment | undefined>()
const [selectedDate, setSelectedDate] = useState<jalaliMoment.Moment>(today)
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false)

const inputRef = useRef<HTMLInputElement | null>(null)
const todoTextRef = useRef<string>('')
const notesRef = useRef<string>('')
const containerRef = useRef<HTMLDivElement>(null)
const datePickerButtonRef = useRef<HTMLButtonElement>(null)
const categoryInputRef = useRef<HTMLInputElement | null>(null)
const notesInputRef = useRef<HTMLInputElement | null>(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) => {
Expand All @@ -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)
}
}
Expand All @@ -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<HTMLInputElement>) => {
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 (
<div ref={containerRef} className="flex-none pt-3 mt-auto">
Expand All @@ -120,8 +135,8 @@ export function ExpandableTodoInput({
<div className="flex-grow w-full">
<TextInput
ref={inputRef}
value={todoText}
onChange={onChangeTodoText}
defaultValue=""
onChange={handleTodoTextChange}
placeholder="عنوان وظیفه جدید..."
className="!h-6 !border-none !outline-none !shadow-none !ring-0 w-full p-0 text-sm !rounded-lg !bg-transparent"
onFocus={handleInputFocus}
Expand All @@ -132,7 +147,7 @@ export function ExpandableTodoInput({
</div>
<Button
onClick={handleAddTodo}
disabled={!todoText.trim() || isAdding}
disabled={isAdding}
loading={isAdding}
loadingText={<IconLoading />}
size="sm"
Expand All @@ -157,7 +172,7 @@ export function ExpandableTodoInput({
<div className="flex items-center flex-1 gap-2 ">
<div
className="relative flex-shrink-0 text-center"
onClick={() => onClickOpenDatePicker()}
onClick={onClickOpenDatePicker}
>
<span className="absolute w-2 h-2 rounded-full -left-0.5 -bottom-0.5 bg-error animate-pulse"></span>
<FiCalendar
Expand All @@ -168,7 +183,7 @@ export function ExpandableTodoInput({
<div className="flex-1">
<button
ref={datePickerButtonRef}
onClick={() => onClickOpenDatePicker()}
onClick={onClickOpenDatePicker}
className="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
Expand Down Expand Up @@ -266,8 +281,9 @@ export function ExpandableTodoInput({
/>
</div>
<TextInput
value={notes}
onChange={setNotes}
ref={notesInputRef}
defaultValue=""
onChange={handleNotesChange}
placeholder="یادداشت یا لینک (اختیاری)"
className="text-xs placeholder:text-xs py-1.5"
debounce={false}
Expand Down
Loading