Skip to content

Commit 6505825

Browse files
x0sinaImMohammad20000
authored andcommitted
refactor(date-picker, user-modal): streamline state management and improve date handling logic
1 parent f9964ed commit 6505825

File tree

2 files changed

+94
-71
lines changed

2 files changed

+94
-71
lines changed

dashboard/src/components/common/date-picker.tsx

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

3-
import * as React from 'react'
43
import { addDays } from 'date-fns'
4+
import { useState, useEffect, useCallback, ChangeEvent, MouseEvent } from 'react'
55
import { Calendar as CalendarIcon, X } from 'lucide-react'
66
import { DateRange } from 'react-day-picker'
77
import { cn } from '@/lib/utils'
@@ -147,9 +147,9 @@ export function DatePicker({
147147
}: DatePickerProps) {
148148
const { t, i18n } = useTranslation()
149149
const isPersianLocale = i18n.language === 'fa'
150-
const [internalOpen, setInternalOpen] = React.useState(false)
151-
const [internalDate, setInternalDate] = React.useState<Date | undefined>(date || undefined)
152-
const [internalRange, setInternalRange] = React.useState<DateRange | undefined>(
150+
const [internalOpen, setInternalOpen] = useState(false)
151+
const [internalDate, setInternalDate] = useState<Date | undefined>(date || undefined)
152+
const [internalRange, setInternalRange] = useState<DateRange | undefined>(
153153
range || defaultRange || (mode === 'range' ? { from: addDays(new Date(), -7), to: new Date() } : undefined),
154154
)
155155

@@ -163,58 +163,63 @@ export function DatePicker({
163163
}
164164

165165
// Sync internal state with props
166-
React.useEffect(() => {
166+
useEffect(() => {
167167
if (date !== undefined) {
168168
setInternalDate(date || undefined)
169169
}
170170
}, [date])
171171

172-
React.useEffect(() => {
172+
useEffect(() => {
173173
if (range !== undefined) {
174174
setInternalRange(range)
175175
}
176176
}, [range])
177177

178-
// Propagate initial range for range mode
179-
React.useEffect(() => {
178+
useEffect(() => {
180179
if (mode === 'range' && internalRange && onRangeChange) {
181180
onRangeChange(internalRange)
182181
}
183-
}, []) // Only on mount
182+
}, [])
184183

185-
const handleDateSelect = React.useCallback(
184+
const handleDateSelect = useCallback(
186185
(selectedDate: Date | undefined) => {
187186
if (!selectedDate) {
188187
setInternalDate(undefined)
189188
onDateChange(undefined)
189+
onFieldChange?.(fieldName, undefined)
190190
return
191191
}
192192

193193
const now = new Date()
194194
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
195195
const selectedDateOnly = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate())
196196

197-
// Ensure date is not in the past (for expiry dates)
198197
if (minDate === undefined && selectedDateOnly < today) {
199198
selectedDate = new Date(now)
200199
}
201200

202-
// Always use current time (not end of day)
203201
selectedDate.setHours(now.getHours(), now.getMinutes(), now.getSeconds(), now.getMilliseconds())
204202

205203
setInternalDate(selectedDate)
206204
const value = useUtcTimestamp ? Math.floor(selectedDate.getTime() / 1000) : getLocalISOTime(selectedDate)
207205
onDateChange(selectedDate)
208206
onFieldChange?.(fieldName, value)
209-
210-
// Close popover when day is clicked
211-
setIsOpen(false)
207+
setTimeout(() => {
208+
setIsOpen(false)
209+
}, 0)
212210
},
213211
[onDateChange, onFieldChange, fieldName, useUtcTimestamp, minDate],
214212
)
215213

216-
const handleTimeChange = React.useCallback(
217-
(e: React.ChangeEvent<HTMLInputElement>) => {
214+
const handleDateSelectWrapper = useCallback(
215+
(selectedDate: Date | undefined) => {
216+
handleDateSelect(selectedDate)
217+
},
218+
[handleDateSelect],
219+
)
220+
221+
const handleTimeChange = useCallback(
222+
(e: ChangeEvent<HTMLInputElement>) => {
218223
e.preventDefault()
219224
e.stopPropagation()
220225
if (internalDate && e.target.value) {
@@ -236,7 +241,7 @@ export function DatePicker({
236241
[internalDate, onDateChange, onFieldChange, fieldName, useUtcTimestamp],
237242
)
238243

239-
const handleRangeSelect = React.useCallback(
244+
const handleRangeSelect = useCallback(
240245
(selectedRange: DateRange | undefined) => {
241246
setInternalRange(selectedRange)
242247
onRangeChange?.(selectedRange)
@@ -249,7 +254,7 @@ export function DatePicker({
249254
[onRangeChange],
250255
)
251256

252-
const formatDate = React.useCallback(
257+
const formatDate = useCallback(
253258
(date: Date) => {
254259
if (customFormatDate) {
255260
return customFormatDate(date)
@@ -259,7 +264,7 @@ export function DatePicker({
259264
[customFormatDate, isPersianLocale, showTime],
260265
)
261266

262-
const dateDisabled = React.useCallback(
267+
const dateDisabled = useCallback(
263268
(date: Date) => {
264269
if (mode === 'range' && disableAfter && date > disableAfter) {
265270
return true
@@ -285,8 +290,8 @@ export function DatePicker({
285290
? `${String(displayDate.getHours()).padStart(2, '0')}:${String(displayDate.getMinutes()).padStart(2, '0')}`
286291
: ''
287292

288-
const handleClear = React.useCallback(
289-
(e: React.MouseEvent) => {
293+
const handleClear = useCallback(
294+
(e: MouseEvent<HTMLButtonElement>) => {
290295
e.preventDefault()
291296
e.stopPropagation()
292297
setInternalDate(undefined)
@@ -326,8 +331,7 @@ export function DatePicker({
326331
<PopoverContent
327332
className="w-auto p-0"
328333
align="start"
329-
onInteractOutside={(e: Event) => {
330-
e.preventDefault()
334+
onInteractOutside={() => {
331335
setIsOpen(false)
332336
}}
333337
onEscapeKeyDown={() => setIsOpen(false)}
@@ -336,7 +340,7 @@ export function DatePicker({
336340
<PersianCalendar
337341
mode="single"
338342
selected={displayDate || undefined}
339-
onSelect={handleDateSelect}
343+
onSelect={handleDateSelectWrapper}
340344
disabled={dateDisabled}
341345
captionLayout="dropdown"
342346
defaultMonth={displayDate || now}
@@ -350,7 +354,7 @@ export function DatePicker({
350354
<Calendar
351355
mode="single"
352356
selected={displayDate || undefined}
353-
onSelect={handleDateSelect}
357+
onSelect={handleDateSelectWrapper}
354358
disabled={dateDisabled}
355359
captionLayout="dropdown"
356360
defaultMonth={displayDate || now}
@@ -363,18 +367,19 @@ export function DatePicker({
363367
)}
364368
{showTime && (
365369
<>
366-
<div className="hidden lg:flex items-center gap-1.5 flex-wrap border-t p-3">
370+
<div className="hidden lg:flex items-center gap-1 flex-wrap border-t p-2">
367371
{[
368-
{ label: '7d', days: 7 },
369-
{ label: '1m', days: 30 },
370-
{ label: '2m', days: 60 },
371-
{ label: '3m', days: 90 },
372-
{ label: '1y', days: 365 },
372+
{ label: '+7d', days: 7 },
373+
{ label: '+1m', days: 30 },
374+
{ label: '+2m', days: 60 },
375+
{ label: '+3m', days: 90 },
376+
{ label: '+1y', days: 365 },
373377
].map(({ label, days }) => {
374378
const handleShortcut = () => {
375-
const targetDate = new Date(now)
376-
targetDate.setDate(now.getDate() + days)
377-
// Use current time instead of end of day
379+
const baseDate = displayDate || now
380+
const targetDate = new Date(baseDate)
381+
targetDate.setDate(baseDate.getDate() + days)
382+
// Preserve time from base date
378383
handleDateSelect(targetDate)
379384
}
380385
return (
@@ -383,7 +388,7 @@ export function DatePicker({
383388
type="button"
384389
variant="ghost"
385390
size="sm"
386-
className="h-7 px-2.5 text-xs text-muted-foreground hover:text-foreground"
391+
className="h-5 px-1.5 text-[10px] text-muted-foreground hover:text-foreground"
387392
onClick={(e) => {
388393
e.preventDefault()
389394
e.stopPropagation()
@@ -400,7 +405,10 @@ export function DatePicker({
400405
type="time"
401406
value={timeValue}
402407
onChange={handleTimeChange}
403-
className="w-full"
408+
className="w-full [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-100 [&::-webkit-calendar-picker-indicator]:invert [&::-webkit-calendar-picker-indicator]:brightness-0 [&::-webkit-calendar-picker-indicator]:saturate-100 [&::-webkit-calendar-picker-indicator]:hue-rotate-0"
409+
style={{
410+
colorScheme: 'dark',
411+
}}
404412
dir="ltr"
405413
/>
406414
</div>

dashboard/src/components/dialogs/user-modal.tsx

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { dateUtils, useRelativeExpiryDate } from '@/utils/dateFormatter'
3131
import { formatBytes, gbToBytes } from '@/utils/formatByte'
3232
import { useQuery, useQueryClient } from '@tanstack/react-query'
3333
import { ChevronDown, Layers, ListStart, Lock, RefreshCcw, Users } from 'lucide-react'
34-
import React, { useEffect, useState, useTransition } from 'react'
34+
import React, { useEffect, useState } from 'react'
3535
import { UseFormReturn } from 'react-hook-form'
3636
import { useTranslation } from 'react-i18next'
3737
import { toast } from 'sonner'
@@ -109,63 +109,66 @@ const ExpiryDateField = ({
109109
}) => {
110110
const { t } = useTranslation()
111111
const expireInfo = useRelativeExpiryDate(displayDate ? Math.floor(displayDate.getTime() / 1000) : null)
112-
const [, startTransition] = useTransition()
113112
const dir = useDirDetection()
114113

115114
const handleDateChange = React.useCallback(
116115
(date: Date | undefined) => {
117116
if (date) {
118117
// Use the same logic as centralized DatePicker
119118
const value = useUtcTimestamp ? Math.floor(date.getTime() / 1000) : getLocalISOTime(date)
120-
startTransition(() => {
121-
field.onChange(value)
122-
handleFieldChange(fieldName, value)
123-
})
119+
field.onChange(value)
120+
handleFieldChange(fieldName, value)
124121
} else {
125-
startTransition(() => {
126-
field.onChange('')
127-
handleFieldChange(fieldName, undefined)
128-
})
122+
field.onChange('')
123+
handleFieldChange(fieldName, undefined)
129124
}
130125
},
131-
[field, handleFieldChange, startTransition, useUtcTimestamp, fieldName],
126+
[field, handleFieldChange, useUtcTimestamp, fieldName],
132127
)
133128

134129
const handleShortcut = React.useCallback(
135130
(days: number) => {
136-
const now = new Date()
137-
const targetDate = new Date(now)
138-
targetDate.setDate(now.getDate() + days)
139-
// Use current time instead of end of day
131+
const baseDate = displayDate || new Date()
132+
const targetDate = new Date(baseDate)
133+
targetDate.setDate(baseDate.getDate() + days)
134+
// Preserve time from base date
140135
handleDateChange(targetDate)
141136
},
142-
[handleDateChange],
137+
[handleDateChange, displayDate],
143138
)
144139

145-
const now = new Date()
146-
const maxDate = new Date(now.getFullYear() + 15, 11, 31)
140+
// Memoize now to start of today to prevent it from changing every second
141+
// This ensures minDate only changes once per day, not on every render
142+
const now = React.useMemo(() => {
143+
const today = new Date()
144+
return new Date(today.getFullYear(), today.getMonth(), today.getDate())
145+
}, [])
146+
147+
const maxDate = React.useMemo(() => {
148+
return new Date(now.getFullYear() + 15, 11, 31)
149+
}, [now])
147150

148151
const shortcuts = [
149-
{ label: '7d', days: 7 },
150-
{ label: '1m', days: 30 },
151-
{ label: '2m', days: 60 },
152-
{ label: '3m', days: 90 },
153-
{ label: '6m', days: 180 },
154-
{ label: '1y', days: 365 },
152+
{ label: '+7d', days: 7 },
153+
{ label: '+1m', days: 30 },
154+
{ label: '+2m', days: 60 },
155+
{ label: '+3m', days: 90 },
156+
{ label: '+6m', days: 180 },
157+
{ label: '+1y', days: 365 },
155158
]
156159

157160
return (
158161
<FormItem className="flex flex-1 flex-col">
159162
<FormLabel className='mb-0.5'>{label}</FormLabel>
160-
<div className="space-y-2">
161-
<div className="flex lg:hidden items-center gap-1.5 flex-wrap">
163+
<div className="space-y-2 lg:!mt-0">
164+
<div className="flex lg:hidden items-center gap-1 flex-wrap">
162165
{shortcuts.map(({ label, days }) => (
163166
<Button
164167
key={label}
165168
type="button"
166169
variant="ghost"
167170
size="sm"
168-
className="h-7 px-2.5 text-xs text-muted-foreground hover:text-foreground"
171+
className="h-5 px-1.5 text-[10px] text-muted-foreground hover:text-foreground"
169172
onClick={(e) => {
170173
e.preventDefault()
171174
e.stopPropagation()
@@ -191,11 +194,14 @@ const ExpiryDateField = ({
191194
fieldName={fieldName}
192195
onFieldChange={handleFieldChange}
193196
/>
194-
{expireInfo && (
195-
<p className={cn('absolute top-full text-end right-0 mt-1 whitespace-nowrap text-xs text-muted-foreground', !expireInfo.time && 'hidden', dir === 'rtl' ? 'right-0' : 'left-0')}>
196-
{expireInfo.time !== '0' && expireInfo.time !== '0s'
197-
? t('expires', { time: expireInfo.time, defaultValue: 'Expires in {{time}}' })
198-
: t('expired', { time: expireInfo.time, defaultValue: 'Expired in {{time}}' })}
197+
{displayDate && expireInfo?.time && (
198+
<p className={cn('absolute top-full lg:w-48 lg:text-ellipsis lg:overflow-hidden text-end right-0 mt-1 whitespace-nowrap text-xs text-muted-foreground', dir === 'rtl' ? 'right-0' : 'left-0')}>
199+
{(() => {
200+
const now = new Date()
201+
const isExpired = displayDate < now
202+
const translationKey = isExpired ? 'expired' : 'expires'
203+
return t(translationKey, { time: expireInfo.time, defaultValue: isExpired ? 'Expired {{time}}' : 'Expires in {{time}}' })
204+
})()}
199205
</p>
200206
)}
201207
</div>
@@ -391,7 +397,9 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
391397
}
392398
}
393399
} else if (typeof value === 'number') {
394-
// Handle Unix timestamp (seconds) using the same logic as other components
400+
if (value <= 0) {
401+
return null
402+
}
395403
try {
396404
const dayjsDate = dateUtils.toDayjs(value)
397405
if (dayjsDate.isValid()) {
@@ -556,14 +564,21 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
556564
handleFieldChange('on_hold_expire_duration', defaultDuration)
557565
}
558566
// Clear expire field when switching to on_hold status
559-
form.setValue('expire', undefined)
567+
form.setValue('expire', '')
568+
handleFieldChange('expire', undefined)
560569
form.clearErrors('expire')
561570
} else {
562571
// Clear on_hold fields when switching away from on_hold status
563572
form.setValue('on_hold_expire_duration', undefined)
564573
form.clearErrors('on_hold_expire_duration')
565574
form.setValue('on_hold_timeout', undefined)
566575
form.clearErrors('on_hold_timeout')
576+
const currentExpire = form.getValues('expire')
577+
if (currentExpire === null || currentExpire === undefined || currentExpire === '' || (typeof currentExpire === 'number' && currentExpire <= 0)) {
578+
form.setValue('expire', '')
579+
handleFieldChange('expire', undefined)
580+
form.clearErrors('expire')
581+
}
567582
}
568583
}, [status, form, t, handleFieldChange])
569584

0 commit comments

Comments
 (0)