ADD new frontend
This commit is contained in:
627
frontend/src/components/ui/DatePicker/DatePicker.tsx
Normal file
627
frontend/src/components/ui/DatePicker/DatePicker.tsx
Normal file
@@ -0,0 +1,627 @@
|
||||
import React, { forwardRef, useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
export interface DatePickerProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
helperText?: string;
|
||||
placeholder?: string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'outline' | 'filled' | 'unstyled';
|
||||
value?: Date | null;
|
||||
defaultValue?: Date | null;
|
||||
onChange?: (date: Date | null) => void;
|
||||
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||
isRequired?: boolean;
|
||||
isInvalid?: boolean;
|
||||
format?: string;
|
||||
locale?: 'es' | 'en';
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
disabledDates?: Date[];
|
||||
disablePast?: boolean;
|
||||
disableFuture?: boolean;
|
||||
showTime?: boolean;
|
||||
showToday?: boolean;
|
||||
showClear?: boolean;
|
||||
closeOnSelect?: boolean;
|
||||
firstDayOfWeek?: 0 | 1; // 0 = Sunday, 1 = Monday
|
||||
monthsToShow?: number;
|
||||
yearRange?: number;
|
||||
renderDay?: (date: Date, isSelected: boolean, isToday: boolean, isDisabled: boolean) => React.ReactNode;
|
||||
renderHeader?: (date: Date, onPrevMonth: () => void, onNextMonth: () => void) => React.ReactNode;
|
||||
}
|
||||
|
||||
const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(({
|
||||
label,
|
||||
error,
|
||||
helperText,
|
||||
placeholder = 'Seleccionar fecha',
|
||||
size = 'md',
|
||||
variant = 'outline',
|
||||
value,
|
||||
defaultValue,
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
isRequired = false,
|
||||
isInvalid = false,
|
||||
format = 'dd/mm/yyyy',
|
||||
locale = 'es',
|
||||
minDate,
|
||||
maxDate,
|
||||
disabledDates = [],
|
||||
disablePast = false,
|
||||
disableFuture = false,
|
||||
showTime = false,
|
||||
showToday = true,
|
||||
showClear = true,
|
||||
closeOnSelect = true,
|
||||
firstDayOfWeek = 1,
|
||||
monthsToShow = 1,
|
||||
yearRange = 100,
|
||||
renderDay,
|
||||
renderHeader,
|
||||
className,
|
||||
id,
|
||||
disabled,
|
||||
...props
|
||||
}, ref) => {
|
||||
const datePickerId = id || `datepicker-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const [internalValue, setInternalValue] = useState<Date | null>(
|
||||
value !== undefined ? value : defaultValue || null
|
||||
);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentMonth, setCurrentMonth] = useState(
|
||||
(value || defaultValue || new Date()).getMonth()
|
||||
);
|
||||
const [currentYear, setCurrentYear] = useState(
|
||||
(value || defaultValue || new Date()).getFullYear()
|
||||
);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [timeValue, setTimeValue] = useState('00:00');
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Use controlled value if provided, otherwise use internal state
|
||||
const currentValue = value !== undefined ? value : internalValue;
|
||||
|
||||
// Localization
|
||||
const translations = {
|
||||
es: {
|
||||
months: [
|
||||
'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
|
||||
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'
|
||||
],
|
||||
weekdays: ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'],
|
||||
today: 'Hoy',
|
||||
clear: 'Limpiar',
|
||||
},
|
||||
en: {
|
||||
months: [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
],
|
||||
weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||
today: 'Today',
|
||||
clear: 'Clear',
|
||||
},
|
||||
};
|
||||
|
||||
const t = translations[locale];
|
||||
|
||||
// Format date for display
|
||||
const formatDate = (date: Date | null): string => {
|
||||
if (!date) return '';
|
||||
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = date.getFullYear().toString();
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
let formatted = format
|
||||
.replace(/dd/g, day)
|
||||
.replace(/mm/g, month)
|
||||
.replace(/yyyy/g, year);
|
||||
|
||||
if (showTime) {
|
||||
formatted += ` ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
return formatted;
|
||||
};
|
||||
|
||||
// Parse date from string
|
||||
const parseDate = (dateString: string): Date | null => {
|
||||
if (!dateString) return null;
|
||||
|
||||
try {
|
||||
const parts = dateString.split(/[/\-\s:]/);
|
||||
|
||||
if (format.startsWith('dd/mm/yyyy')) {
|
||||
const day = parseInt(parts[0], 10);
|
||||
const month = parseInt(parts[1], 10) - 1;
|
||||
const year = parseInt(parts[2], 10);
|
||||
|
||||
if (!isNaN(day) && !isNaN(month) && !isNaN(year)) {
|
||||
const date = new Date(year, month, day);
|
||||
|
||||
if (showTime && parts.length >= 5) {
|
||||
const hours = parseInt(parts[3], 10) || 0;
|
||||
const minutes = parseInt(parts[4], 10) || 0;
|
||||
date.setHours(hours, minutes);
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Check if date is disabled
|
||||
const isDateDisabled = (date: Date): boolean => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const checkDate = new Date(date);
|
||||
checkDate.setHours(0, 0, 0, 0);
|
||||
|
||||
if (disablePast && checkDate < today) return true;
|
||||
if (disableFuture && checkDate > today) return true;
|
||||
if (minDate && checkDate < minDate) return true;
|
||||
if (maxDate && checkDate > maxDate) return true;
|
||||
|
||||
return disabledDates.some(disabledDate => {
|
||||
const disabled = new Date(disabledDate);
|
||||
disabled.setHours(0, 0, 0, 0);
|
||||
return disabled.getTime() === checkDate.getTime();
|
||||
});
|
||||
};
|
||||
|
||||
// Check if date is today
|
||||
const isToday = (date: Date): boolean => {
|
||||
const today = new Date();
|
||||
return date.getDate() === today.getDate() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getFullYear() === today.getFullYear();
|
||||
};
|
||||
|
||||
// Check if date is selected
|
||||
const isSelected = (date: Date): boolean => {
|
||||
if (!currentValue) return false;
|
||||
return date.getDate() === currentValue.getDate() &&
|
||||
date.getMonth() === currentValue.getMonth() &&
|
||||
date.getFullYear() === currentValue.getFullYear();
|
||||
};
|
||||
|
||||
// Get calendar days for current month
|
||||
const getCalendarDays = useMemo(() => {
|
||||
const firstDay = new Date(currentYear, currentMonth, 1);
|
||||
const lastDay = new Date(currentYear, currentMonth + 1, 0);
|
||||
const startDate = new Date(firstDay);
|
||||
const endDate = new Date(lastDay);
|
||||
|
||||
// Adjust for first day of week
|
||||
const dayOfWeek = firstDay.getDay();
|
||||
const adjustedDayOfWeek = firstDayOfWeek === 0 ? dayOfWeek : (dayOfWeek + 6) % 7;
|
||||
startDate.setDate(1 - adjustedDayOfWeek);
|
||||
|
||||
// Adjust end date to complete the grid
|
||||
const daysToAdd = 6 - ((endDate.getDay() - firstDayOfWeek + 7) % 7);
|
||||
endDate.setDate(endDate.getDate() + daysToAdd);
|
||||
|
||||
const days: Date[] = [];
|
||||
const current = new Date(startDate);
|
||||
|
||||
while (current <= endDate) {
|
||||
days.push(new Date(current));
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
|
||||
return days;
|
||||
}, [currentMonth, currentYear, firstDayOfWeek]);
|
||||
|
||||
// Handle date selection
|
||||
const handleDateSelect = (date: Date) => {
|
||||
if (isDateDisabled(date)) return;
|
||||
|
||||
let newDate = new Date(date);
|
||||
|
||||
// Preserve time if time is shown and a previous value exists
|
||||
if (showTime && currentValue) {
|
||||
newDate.setHours(currentValue.getHours(), currentValue.getMinutes());
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
setInternalValue(newDate);
|
||||
}
|
||||
onChange?.(newDate);
|
||||
|
||||
if (closeOnSelect && !showTime) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle today button click
|
||||
const handleTodayClick = () => {
|
||||
const today = new Date();
|
||||
if (showTime && currentValue) {
|
||||
today.setHours(currentValue.getHours(), currentValue.getMinutes());
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
setInternalValue(today);
|
||||
}
|
||||
onChange?.(today);
|
||||
setCurrentMonth(today.getMonth());
|
||||
setCurrentYear(today.getFullYear());
|
||||
|
||||
if (closeOnSelect && !showTime) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle clear button click
|
||||
const handleClear = () => {
|
||||
if (value === undefined) {
|
||||
setInternalValue(null);
|
||||
}
|
||||
onChange?.(null);
|
||||
setInputValue('');
|
||||
};
|
||||
|
||||
// Handle input change
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setInputValue(newValue);
|
||||
|
||||
const parsedDate = parseDate(newValue);
|
||||
if (parsedDate && !isNaN(parsedDate.getTime())) {
|
||||
if (value === undefined) {
|
||||
setInternalValue(parsedDate);
|
||||
}
|
||||
onChange?.(parsedDate);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle time change
|
||||
const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newTime = e.target.value;
|
||||
setTimeValue(newTime);
|
||||
|
||||
if (currentValue) {
|
||||
const [hours, minutes] = newTime.split(':').map(num => parseInt(num, 10));
|
||||
const newDate = new Date(currentValue);
|
||||
newDate.setHours(hours, minutes);
|
||||
|
||||
if (value === undefined) {
|
||||
setInternalValue(newDate);
|
||||
}
|
||||
onChange?.(newDate);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle navigation
|
||||
const handlePrevMonth = () => {
|
||||
if (currentMonth === 0) {
|
||||
setCurrentMonth(11);
|
||||
setCurrentYear(currentYear - 1);
|
||||
} else {
|
||||
setCurrentMonth(currentMonth - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextMonth = () => {
|
||||
if (currentMonth === 11) {
|
||||
setCurrentMonth(0);
|
||||
setCurrentYear(currentYear + 1);
|
||||
} else {
|
||||
setCurrentMonth(currentMonth + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Close calendar when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Update input value when currentValue changes
|
||||
useEffect(() => {
|
||||
if (currentValue) {
|
||||
setInputValue(formatDate(currentValue));
|
||||
if (showTime) {
|
||||
setTimeValue(`${currentValue.getHours().toString().padStart(2, '0')}:${currentValue.getMinutes().toString().padStart(2, '0')}`);
|
||||
}
|
||||
} else {
|
||||
setInputValue('');
|
||||
}
|
||||
}, [currentValue, format, showTime]);
|
||||
|
||||
const hasError = isInvalid || !!error;
|
||||
|
||||
const baseInputClasses = [
|
||||
'w-full transition-colors duration-200',
|
||||
'focus:outline-none',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
'placeholder:text-input-placeholder'
|
||||
];
|
||||
|
||||
const variantClasses = {
|
||||
outline: [
|
||||
'bg-input-bg border border-input-border',
|
||||
'focus:border-input-border-focus focus:ring-1 focus:ring-input-border-focus',
|
||||
hasError ? 'border-input-border-error focus:border-input-border-error focus:ring-input-border-error' : ''
|
||||
],
|
||||
filled: [
|
||||
'bg-bg-secondary border border-transparent',
|
||||
'focus:bg-input-bg focus:border-input-border-focus',
|
||||
hasError ? 'border-input-border-error' : ''
|
||||
],
|
||||
unstyled: [
|
||||
'bg-transparent border-none',
|
||||
'focus:ring-0'
|
||||
]
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'h-8 px-3 text-sm',
|
||||
md: 'h-10 px-4 text-base',
|
||||
lg: 'h-12 px-5 text-lg'
|
||||
};
|
||||
|
||||
const inputClasses = clsx(
|
||||
baseInputClasses,
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
'rounded-lg pr-10',
|
||||
className
|
||||
);
|
||||
|
||||
const calendarClasses = clsx(
|
||||
'absolute top-full left-0 z-50 mt-1 bg-dropdown-bg border border-dropdown-border rounded-lg shadow-lg',
|
||||
'transform transition-all duration-200 ease-out',
|
||||
{
|
||||
'opacity-0 scale-95 pointer-events-none': !isOpen,
|
||||
'opacity-100 scale-100': isOpen,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={datePickerId}
|
||||
className="block text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
{label}
|
||||
{isRequired && (
|
||||
<span className="text-color-error ml-1">*</span>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div ref={containerRef} className="relative">
|
||||
<input
|
||||
ref={ref || inputRef}
|
||||
id={datePickerId}
|
||||
type="text"
|
||||
className={inputClasses}
|
||||
placeholder={placeholder}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
setIsOpen(true);
|
||||
onFocus?.(e);
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
aria-invalid={hasError}
|
||||
aria-describedby={
|
||||
error ? `${datePickerId}-error` :
|
||||
helperText ? `${datePickerId}-helper` :
|
||||
undefined
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-text-tertiary hover:text-text-primary transition-colors duration-150"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Calendar Popup */}
|
||||
<div className={calendarClasses}>
|
||||
<div className="p-4">
|
||||
{/* Header */}
|
||||
{renderHeader ? (
|
||||
renderHeader(new Date(currentYear, currentMonth), handlePrevMonth, handleNextMonth)
|
||||
) : (
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-text-tertiary hover:text-text-primary transition-colors duration-150"
|
||||
onClick={handlePrevMonth}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
className="px-2 py-1 text-sm border border-border-primary rounded bg-input-bg focus:outline-none focus:ring-2 focus:ring-color-primary/20"
|
||||
value={currentMonth}
|
||||
onChange={(e) => setCurrentMonth(parseInt(e.target.value))}
|
||||
>
|
||||
{t.months.map((month, index) => (
|
||||
<option key={index} value={index}>
|
||||
{month}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
className="px-2 py-1 text-sm border border-border-primary rounded bg-input-bg focus:outline-none focus:ring-2 focus:ring-color-primary/20"
|
||||
value={currentYear}
|
||||
onChange={(e) => setCurrentYear(parseInt(e.target.value))}
|
||||
>
|
||||
{Array.from({ length: yearRange * 2 + 1 }, (_, i) => {
|
||||
const year = new Date().getFullYear() - yearRange + i;
|
||||
return (
|
||||
<option key={year} value={year}>
|
||||
{year}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-text-tertiary hover:text-text-primary transition-colors duration-150"
|
||||
onClick={handleNextMonth}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Weekdays */}
|
||||
<div className="grid grid-cols-7 gap-1 mb-2">
|
||||
{(firstDayOfWeek === 0 ? t.weekdays : [...t.weekdays.slice(1), t.weekdays[0]]).map((day) => (
|
||||
<div key={day} className="text-xs font-medium text-text-tertiary text-center p-2">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Calendar Grid */}
|
||||
<div className="grid grid-cols-7 gap-1">
|
||||
{getCalendarDays.map((date) => {
|
||||
const isCurrentMonth = date.getMonth() === currentMonth;
|
||||
const selected = isSelected(date);
|
||||
const today = isToday(date);
|
||||
const disabled = isDateDisabled(date);
|
||||
|
||||
const dayContent = renderDay ? (
|
||||
renderDay(date, selected, today, disabled)
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'w-8 h-8 text-sm rounded-full transition-colors duration-150 hover:bg-bg-secondary',
|
||||
{
|
||||
'text-text-tertiary': !isCurrentMonth,
|
||||
'text-text-primary': isCurrentMonth && !selected && !today,
|
||||
'bg-color-primary text-text-inverse': selected,
|
||||
'bg-color-primary/10 text-color-primary font-medium': today && !selected,
|
||||
'opacity-50 cursor-not-allowed': disabled,
|
||||
'hover:bg-bg-secondary': !disabled && !selected,
|
||||
}
|
||||
)}
|
||||
onClick={() => handleDateSelect(date)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{date.getDate()}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={date.toString()} className="flex items-center justify-center">
|
||||
{dayContent}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Time Input */}
|
||||
{showTime && (
|
||||
<div className="mt-4 pt-4 border-t border-border-primary">
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">
|
||||
Hora
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={timeValue}
|
||||
onChange={handleTimeChange}
|
||||
className="w-full px-3 py-2 border border-input-border rounded bg-input-bg focus:outline-none focus:ring-2 focus:ring-color-primary/20"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
{(showToday || showClear) && (
|
||||
<div className="flex items-center justify-between mt-4 pt-4 border-t border-border-primary">
|
||||
{showToday && (
|
||||
<button
|
||||
type="button"
|
||||
className="px-3 py-1 text-sm text-color-primary hover:bg-color-primary/10 rounded transition-colors duration-150"
|
||||
onClick={handleTodayClick}
|
||||
>
|
||||
{t.today}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showClear && (
|
||||
<button
|
||||
type="button"
|
||||
className="px-3 py-1 text-sm text-text-tertiary hover:text-color-error hover:bg-color-error/10 rounded transition-colors duration-150"
|
||||
onClick={handleClear}
|
||||
>
|
||||
{t.clear}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p
|
||||
id={`${datePickerId}-error`}
|
||||
className="mt-2 text-sm text-color-error"
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{helperText && !error && (
|
||||
<p
|
||||
id={`${datePickerId}-helper`}
|
||||
className="mt-2 text-sm text-text-secondary"
|
||||
>
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
DatePicker.displayName = 'DatePicker';
|
||||
|
||||
export default DatePicker;
|
||||
3
frontend/src/components/ui/DatePicker/index.ts
Normal file
3
frontend/src/components/ui/DatePicker/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default } from './DatePicker';
|
||||
export { default as DatePicker } from './DatePicker';
|
||||
export type { DatePickerProps } from './DatePicker';
|
||||
Reference in New Issue
Block a user