From f6d4ef4697909dfbbbe02e7adb16c092e62695a6 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 3 Nov 2025 10:27:51 +0700 Subject: [PATCH] chore: update DateInput component --- src/components/input/DateInput.tsx | 282 ++++++++++++++++++++++++----- 1 file changed, 241 insertions(+), 41 deletions(-) diff --git a/src/components/input/DateInput.tsx b/src/components/input/DateInput.tsx index 6e2f1d77..92d28397 100644 --- a/src/components/input/DateInput.tsx +++ b/src/components/input/DateInput.tsx @@ -1,14 +1,23 @@ 'use client'; -import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react'; - -import { cn } from '@/lib/helper'; +import { + ChangeEventHandler, + FocusEventHandler, + useEffect, + useState, +} from 'react'; +import { cn, formatDate } from '@/lib/helper'; +import Modal, { useModal } from '../Modal'; +import { DateRange, DayPicker, Matcher } from 'react-day-picker'; +import 'react-day-picker/dist/style.css'; +import Button from '../Button'; +import { Icon } from '@iconify/react'; export interface DateInputProps { label?: string; bottomLabel?: string; name: string; - value?: string; + value?: string | { from?: string; to?: string }; placeholder?: string; min?: string; max?: string; @@ -24,9 +33,8 @@ export interface DateInputProps { readOnly?: boolean; required?: boolean; isLoading?: boolean; + isRange?: boolean; errorMessage?: string; - startAdornment?: ReactNode; - endAdornment?: ReactNode; onChange?: ChangeEventHandler; onBlur?: FocusEventHandler; } @@ -36,22 +44,144 @@ const DateInput = ({ bottomLabel, name, value, - placeholder, + placeholder = 'dd/mm/yyyy', min, max, className, - isError, - isValid, - errorMessage, - startAdornment, - endAdornment, + isError: externalError, + isValid: externalValid, + errorMessage: externalErrorMessage, disabled = false, required = false, onChange, onBlur, readOnly = false, isLoading = false, + isRange = false, }: DateInputProps) => { + const [internalError, setInternalError] = useState(null); + const [selected, setSelected] = useState(); + const [selectedRange, setSelectedRange] = useState<{ + from?: Date; + to?: Date; + }>({}); + const [displayValue, setDisplayValue] = useState(''); + + const minDate = min + ? new Date(min.split('/').reverse().join('-')) + : undefined; + const maxDate = max + ? new Date(max.split('/').reverse().join('-')) + : undefined; + + const calendarModal = useModal(); + + // --- Sync value props --- + useEffect(() => { + if (!value) return; + if (isRange && typeof value === 'object') { + const from = value.from ? new Date(value.from) : undefined; + const to = value.to ? new Date(value.to) : undefined; + setSelectedRange({ from, to }); + setDisplayValue( + `${from ? formatDate(from, 'DD/MM/YYYY') : ''} ${ + to ? '- ' + formatDate(to, 'DD/MM/YYYY') : '' + }` + ); + } else if (typeof value === 'string') { + const iso = value.includes('/') + ? value.split('/').reverse().join('-') + : value; + const date = new Date(iso); + setSelected(date); + setDisplayValue(formatDate(iso, 'DD/MM/YYYY')); + } + }, [value, isRange]); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + if (!disabled && !readOnly) calendarModal.openModal(); + }; + + const handleBlur: FocusEventHandler = (e) => { + onBlur?.(e); + }; + + const handleSelectSingle = (selectedDate?: Date) => { + if (!selectedDate) return; + if (minDate && selectedDate < minDate) { + setInternalError(`Tanggal tidak boleh sebelum ${min}`); + return; + } + if (maxDate && selectedDate > maxDate) { + setInternalError(`Tanggal tidak boleh setelah ${max}`); + return; + } + setInternalError(null); + setSelected(selectedDate); + const formattedDisplay = formatDate(selectedDate, 'DD/MM/YYYY'); + const formattedISO = formatDate(selectedDate, 'YYYY-MM-DD'); + setDisplayValue(formattedDisplay); + + const syntheticEvent = { + target: { name, value: formattedISO }, + } as unknown as React.ChangeEvent; + onChange?.(syntheticEvent); + calendarModal.closeModal(); + }; + + const handleSelectRange = (range?: { from?: Date; to?: Date }) => { + if (!range) return; + setSelectedRange(range); + + const fromStr = range.from ? formatDate(range.from, 'DD/MM/YYYY') : ''; + const toStr = range.to ? formatDate(range.to, 'DD/MM/YYYY') : ''; + setDisplayValue(`${fromStr}${toStr ? ' - ' + toStr : ''}`); + + // Jika kedua tanggal sudah terpilih + if (range.from && range.to) { + if (minDate && range.from < minDate) { + setInternalError(`Tanggal mulai tidak boleh sebelum ${min}`); + return; + } + if (maxDate && range.to > maxDate) { + setInternalError(`Tanggal akhir tidak boleh setelah ${max}`); + return; + } + + setInternalError(null); + const syntheticEvent = { + target: { + name, + value: { + from: formatDate(range.from, 'YYYY-MM-DD'), + to: formatDate(range.to, 'YYYY-MM-DD'), + }, + }, + } as unknown as React.ChangeEvent; + onChange?.(syntheticEvent); + } + }; + + const handleResetDate = () => { + setSelected(undefined); + setSelectedRange({}); + setDisplayValue(''); + const syntheticEvent = { + target: { name, value: isRange ? { from: '', to: '' } : '' }, + } as unknown as React.ChangeEvent; + onChange?.(syntheticEvent); + calendarModal.closeModal(); + }; + + const handleSaveDate = () => { + if (internalError) return; + calendarModal.closeModal(); + }; + + const finalIsError = externalError || !!internalError; + const finalErrorMessage = internalError || externalErrorMessage; + return (
{label} {required && ( - <> - {' '} - - * - - + + * + )} )}
- {startAdornment && startAdornment} - - {(isLoading || endAdornment) && ( + {isLoading && (
- {isLoading && } - {endAdornment && endAdornment} +
)} + + handleClick(e as unknown as React.MouseEvent) + } + />
- {!isError && bottomLabel && ( + {!finalIsError && bottomLabel && (

{bottomLabel}

)} - {isError && errorMessage && ( -

{errorMessage}

+ {finalIsError && finalErrorMessage && ( +

{finalErrorMessage}

)} + + + {isRange ? ( + {displayValue}
} + disabled={ + [ + minDate ? { before: minDate } : undefined, + maxDate ? { after: maxDate } : undefined, + ].filter(Boolean) as Matcher[] + } + /> + ) : ( + + )} +
+ {isRange && ( + + Tekan dua kali untuk memilih tanggal awal + + )} + +
+ + {isRange && ( + + )} +
+
+ ); };