mediahub-fe/components/form/shared/form-date-picker.tsx

231 lines
6.4 KiB
TypeScript

/**
* FormDatePicker Component
*
* A reusable date picker component that handles:
* - Single date selection
* - Date range selection
* - Label
* - Controller (react-hook-form)
* - Error handling
*
* This component reduces code duplication for date picker fields across forms.
*/
import React from 'react';
import { Control, Controller, FieldError, FieldPath, FieldValues } from 'react-hook-form';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { CalendarIcon } from 'lucide-react';
import { format } from 'date-fns';
import { DateRange } from 'react-day-picker';
import { cn } from '@/lib/utils';
// =============================================================================
// TYPES
// =============================================================================
export interface FormDatePickerProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> {
// Form control
control: Control<TFieldValues>;
name: TName;
// Field configuration
label?: string;
placeholder?: string;
required?: boolean;
// Date picker configuration
mode?: 'single' | 'range';
numberOfMonths?: number;
// Styling
className?: string;
labelClassName?: string;
buttonClassName?: string;
errorClassName?: string;
// Validation
validation?: {
required?: boolean | string;
};
// Additional props
disabled?: boolean;
size?: 'sm' | 'md' | 'lg';
// Date formatting
dateFormat?: string;
// Custom render functions
renderTrigger?: (props: {
field: any;
fieldState: { error?: FieldError };
selectedDate: Date | DateRange | undefined;
placeholder: string;
}) => React.ReactElement;
}
// =============================================================================
// COMPONENT
// =============================================================================
export function FormDatePicker<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
control,
name,
label,
placeholder = 'Pick a date',
required = false,
mode = 'single',
numberOfMonths = 1,
className = '',
labelClassName = '',
buttonClassName = '',
errorClassName = '',
validation,
disabled = false,
size = 'md',
dateFormat = 'LLL dd, y',
renderTrigger,
}: FormDatePickerProps<TFieldValues, TName>) {
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
const getButtonSize = (size: 'sm' | 'md' | 'lg') => {
switch (size) {
case 'sm':
return 'h-8 text-sm';
case 'lg':
return 'h-12 text-base';
default:
return 'h-10 text-sm';
}
};
const formatDate = (date: Date | DateRange | undefined): string => {
if (!date) return '';
if (mode === 'range' && 'from' in date) {
if (date.from) {
if (date.to) {
return `${format(date.from, dateFormat)} - ${format(date.to, dateFormat)}`;
}
return format(date.from, dateFormat);
}
} else if (date instanceof Date) {
return format(date, dateFormat);
}
return '';
};
// =============================================================================
// RENDER FUNCTIONS
// =============================================================================
const renderDefaultTrigger = (
field: any,
fieldState: { error?: FieldError },
selectedDate: Date | DateRange | undefined,
placeholder: string
) => (
<Button
variant="outline"
className={cn(
'w-full justify-start text-left font-normal transition-colors',
getButtonSize(size),
fieldState.error && 'border-destructive focus:border-destructive',
!selectedDate && 'text-muted-foreground',
buttonClassName
)}
disabled={disabled}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{formatDate(selectedDate) || placeholder}
</Button>
);
// =============================================================================
// MAIN RENDER
// =============================================================================
return (
<div className={cn('space-y-2', className)}>
{/* Label */}
{label && (
<Label
htmlFor={name}
className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
required && 'after:content-["*"] after:ml-0.5 after:text-destructive',
labelClassName
)}
>
{label}
</Label>
)}
{/* Controller */}
<Controller
control={control}
name={name}
rules={{
required: validation?.required || required,
}}
render={({ field, fieldState }) => {
const selectedDate = field.value;
return (
<div className="space-y-1">
<Popover>
<PopoverTrigger asChild>
{renderTrigger ?
renderTrigger({ field, fieldState, selectedDate, placeholder }) :
renderDefaultTrigger(field, fieldState, selectedDate, placeholder)
}
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
initialFocus
mode={mode}
defaultMonth={mode === 'range' && selectedDate && 'from' in selectedDate ? selectedDate.from : selectedDate}
selected={selectedDate}
onSelect={field.onChange}
numberOfMonths={numberOfMonths}
disabled={disabled}
/>
</PopoverContent>
</Popover>
{/* Error Message */}
{fieldState.error && (
<p className={cn(
'text-sm text-destructive',
errorClassName
)}>
{fieldState.error.message}
</p>
)}
</div>
);
}}
/>
</div>
);
}
export default FormDatePicker;