import type React from "react" import { useEffect, useMemo, useState, useCallback, useRef } from "react" import { ChevronLeft, ChevronRight, Clock } from "lucide-react" import { DayPicker } from "react-day-picker" import { useVirtualizer } from "@tanstack/react-virtual" import { cn } from "@/lib/utils" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select" import { Button } from "@/app/_components/ui/button" // Custom calendar component with enhanced year and month navigation export function DateTimePicker({ selected, onSelect, disabled, fromYear = 1900, toYear = new Date().getFullYear() + 10, showTimePicker = true, className, minuteStep = 1, showSeconds = true, }: { selected?: Date onSelect: (date?: Date) => void disabled?: (date: Date) => boolean fromYear?: number toYear?: number showTimePicker?: boolean className?: string minuteStep?: number showSeconds?: boolean }) { // Initialize with selected date or current date const [date, setDate] = useState(() => { return selected ? new Date(selected) : new Date() }) const [hours, setHours] = useState(() => { return selected ? String(selected.getHours()).padStart(2, "0") : String(new Date().getHours()).padStart(2, "0") }) const [minutes, setMinutes] = useState(() => { if (selected) { return String(selected.getMinutes()).padStart(2, "0") } // Round current minutes to nearest step const currentMinutes = new Date().getMinutes() const roundedMinutes = Math.round(currentMinutes / minuteStep) * minuteStep return String(roundedMinutes % 60).padStart(2, "0") }) const [seconds, setSeconds] = useState(() => { return selected ? String(selected.getSeconds()).padStart(2, "0") : String(0).padStart(2, "0") }) // Track if we're in the middle of an update to prevent loops const isUpdatingRef = useRef(false) // Generate valid minute options based on minuteStep const minuteOptions = useMemo(() => { const options = [] for (let i = 0; i < 60; i += minuteStep) { options.push(String(i).padStart(2, "0")) } return options }, [minuteStep]) // Generate valid hour options const hourOptions = useMemo(() => { return Array.from({ length: 24 }, (_, i) => String(i).padStart(2, "0")) }, []) // Generate valid second options const secondOptions = useMemo(() => { return Array.from({ length: 60 }, (_, i) => String(i).padStart(2, "0")) }, []) // Update the parent component when date or time changes useEffect(() => { if (isUpdatingRef.current) return if (date) { const newDate = new Date(date) const newHours = Number.parseInt(hours, 10) const newMinutes = Number.parseInt(minutes, 10) const newSeconds = Number.parseInt(seconds, 10) newDate.setHours(newHours, newMinutes, newSeconds, 0) // Only call onSelect if the date actually changed if (!selected || Math.abs(newDate.getTime() - (selected?.getTime() || 0)) > 100) { isUpdatingRef.current = true onSelect(newDate) // Use requestAnimationFrame instead of setTimeout for better performance requestAnimationFrame(() => { isUpdatingRef.current = false }) } } else { onSelect(undefined) } }, [date, hours, minutes, seconds, onSelect, selected]) // Update internal state when selected prop changes useEffect(() => { if (isUpdatingRef.current) return if (selected) { setDate(new Date(selected)) setHours(String(selected.getHours()).padStart(2, "0")) setMinutes(String(selected.getMinutes()).padStart(2, "0")) setSeconds(String(selected.getSeconds()).padStart(2, "0")) } }, [selected]) // Generate years array from fromYear to toYear const years = useMemo(() => { const yearsArray = [] for (let i = toYear; i >= fromYear; i--) { yearsArray.push(i) } return yearsArray }, [fromYear, toYear]) const months = useMemo( () => [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ], [], ) // Handle time input changes const handleHoursChange = useCallback((e: React.ChangeEvent) => { let value = e.target.value.replace(/\D/g, "") if (value === "") { setHours("00") return } const numValue = Number.parseInt(value, 10) if (numValue > 23) { value = "23" } setHours(value.padStart(2, "0")) }, []) const handleMinutesChange = useCallback((e: React.ChangeEvent) => { let value = e.target.value.replace(/\D/g, "") if (value === "") { setMinutes("00") return } const numValue = Number.parseInt(value, 10) if (numValue > 59) { value = "59" } setMinutes(value.padStart(2, "0")) }, []) const handleSecondsChange = useCallback((e: React.ChangeEvent) => { let value = e.target.value.replace(/\D/g, "") if (value === "") { setSeconds("00") return } const numValue = Number.parseInt(value, 10) if (numValue > 59) { value = "59" } setSeconds(value.padStart(2, "0")) }, []) // Clear date selection const handleClear = useCallback(() => { const now = new Date() setDate(now) setHours("00") setMinutes("00") setSeconds("00") onSelect(new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0)) }, [onSelect]) // Set date to now const handleSetNow = useCallback(() => { const now = new Date() setDate(now) setHours(String(now.getHours()).padStart(2, "0")) const roundedMinutes = Math.round(now.getMinutes() / minuteStep) * minuteStep setMinutes(String(roundedMinutes % 60).padStart(2, "0")) setSeconds(String(now.getSeconds()).padStart(2, "0")) onSelect(now) }, [minuteStep, onSelect]) // Custom caption component with optimized year selection const CustomCaption = useCallback( ({ displayMonth }: { displayMonth: Date }) => { const month = displayMonth.getMonth() const year = displayMonth.getFullYear() const yearListRef = useRef(null) const handleMonthChange = useCallback( (newMonth: string) => { const monthIndex = months.findIndex((m) => m === newMonth) const newDate = new Date(date) newDate.setMonth(monthIndex) setDate(newDate) }, [date], ) const handleYearChange = useCallback( (newYear: string) => { const newDate = new Date(date) newDate.setFullYear(Number.parseInt(newYear)) setDate(newDate) }, [date], ) // Create a virtualizer for the years list const virtualizer = useVirtualizer({ count: years.length, getScrollElement: () => yearListRef.current, estimateSize: () => 36, // Approximate height of each year item overscan: 5, // Reduced overscan for better performance }) // Pre-scroll to current year when the select opens const handleYearSelectOpen = useCallback(() => { const yearIndex = years.findIndex((y) => y === year) if (yearIndex !== -1 && yearListRef.current) { // Use requestAnimationFrame for smoother scrolling requestAnimationFrame(() => { virtualizer.scrollToIndex(yearIndex, { align: "center" }) }) } }, [virtualizer, years, year]) return (
) }, [date, months, years], ) return (
newDate && setDate(newDate)} disabled={disabled} month={date} onMonthChange={setDate} components={{ IconLeft: () => , IconRight: () => , Caption: CustomCaption, }} classNames={{ caption: "flex justify-center relative items-center", nav: "space-x-1 flex items-center", nav_button: cn( "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 hover:bg-muted rounded-md transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", ), nav_button_previous: "absolute left-1", nav_button_next: "absolute right-1", table: "w-full border-collapse space-y-1", head_row: "flex", head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", row: "flex w-full mt-2", cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", day: cn( "h-9 w-9 p-0 font-normal aria-selected:opacity-100 hover:bg-accent hover:text-accent-foreground rounded-md transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", ), day_selected: "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", day_today: "bg-accent text-accent-foreground", day_outside: "text-muted-foreground opacity-50", day_disabled: "text-muted-foreground opacity-50", day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground", day_hidden: "invisible", }} /> {showTimePicker && (
: {showSeconds && ( <> : )}
)}
) }