Spaces:
Sleeping
Sleeping
| import { useState, useRef, useEffect } from "react"; | |
| import { format, parse } from "date-fns"; | |
| import { CalendarIcon } from "lucide-react"; | |
| import { Calendar } from "@/components/ui/calendar"; | |
| import { cn } from "@/lib/utils"; | |
| interface CustomDateInputProps { | |
| value: string | null; | |
| onChange: (date: string | null) => void; | |
| placeholder?: string; | |
| label?: string; | |
| disabled?: boolean; | |
| minDate?: Date; | |
| maxDate?: Date; | |
| className?: string; | |
| error?: string; | |
| } | |
| export function CustomDateInput({ | |
| value, | |
| onChange, | |
| placeholder = "Pick a date", | |
| label, | |
| disabled = false, | |
| minDate, | |
| maxDate, | |
| className, | |
| error | |
| }: CustomDateInputProps) { | |
| const [open, setOpen] = useState(false); | |
| const datePickerRef = useRef<HTMLDivElement>(null); | |
| // Convert string date to Date object for the calendar | |
| const getDateValue = (): Date | undefined => { | |
| if (!value) return undefined; | |
| try { | |
| return parse(value, "yyyy-MM-dd", new Date()); | |
| } catch (e) { | |
| console.error("Invalid date format:", value); | |
| return undefined; | |
| } | |
| }; | |
| // Update the date value as string in yyyy-MM-dd format | |
| const handleDateSelect = (date: Date | undefined) => { | |
| if (!date) { | |
| onChange(null); | |
| return; | |
| } | |
| try { | |
| const formattedDate = format(date, "yyyy-MM-dd"); | |
| onChange(formattedDate); | |
| setOpen(false); | |
| } catch (e) { | |
| console.error("Error formatting date:", e); | |
| } | |
| }; | |
| // Format date for display | |
| const getDisplayDate = (): string => { | |
| if (!value) return placeholder; | |
| try { | |
| const date = parse(value, "yyyy-MM-dd", new Date()); | |
| return format(date, "PPP"); // Human readable format | |
| } catch (e) { | |
| console.error("Error parsing date for display:", e); | |
| return value; | |
| } | |
| }; | |
| // Close date picker when clicking outside | |
| useEffect(() => { | |
| const handleClickOutside = (event: MouseEvent) => { | |
| if (datePickerRef.current && !datePickerRef.current.contains(event.target as Node)) { | |
| setOpen(false); | |
| } | |
| }; | |
| if (open) { | |
| document.addEventListener("mousedown", handleClickOutside); | |
| } | |
| return () => { | |
| document.removeEventListener("mousedown", handleClickOutside); | |
| }; | |
| }, [open]); | |
| const toggleOpen = (e: React.MouseEvent) => { | |
| if (disabled) return; | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| setOpen(!open); | |
| }; | |
| return ( | |
| <div className={cn("w-full space-y-2", className)} ref={datePickerRef}> | |
| {label && <div className="font-medium text-sm">{label}</div>} | |
| <div className="relative w-full"> | |
| <div | |
| className={cn( | |
| "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer", | |
| open && "ring-2 ring-ring ring-offset-2", | |
| disabled && "opacity-50 cursor-not-allowed", | |
| !value && "text-muted-foreground" | |
| )} | |
| onClick={toggleOpen} | |
| > | |
| {getDisplayDate()} | |
| <CalendarIcon className="h-4 w-4 opacity-50" /> | |
| </div> | |
| {open && ( | |
| <div | |
| className="absolute z-[10000] mt-1 w-auto rounded-md border bg-popover p-0 text-popover-foreground shadow-md" | |
| > | |
| <Calendar | |
| mode="single" | |
| selected={getDateValue()} | |
| onSelect={handleDateSelect} | |
| disabled={(date) => { | |
| if (minDate && date < minDate) return true; | |
| if (maxDate && date > maxDate) return true; | |
| return false; | |
| }} | |
| initialFocus | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| {error && <p className="text-sm font-medium text-destructive">{error}</p>} | |
| </div> | |
| ); | |
| } |