"use client"; import * as React from "react"; import { format, isValid } from "date-fns"; import { Calendar as CalendarIcon, Clock } from "lucide-react"; import { cn } from "../../lib/utils"; import { Button } from "./button"; import { Calendar } from "./calendar"; import { Popover, PopoverContent, PopoverTrigger, } from "./popover"; import { cva, type VariantProps } from "class-variance-authority"; import { motion, AnimatePresence } from "framer-motion"; // Define pageTransitions locally since micro-interactions might not exist const pageTransitions = { initial: { opacity: 0, x: 20 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, x: -20 } }; /** * DatePicker Component * * A comprehensive date picker with calendar interface */ const datePickerVariants = cva( "w-full justify-start text-left font-normal", { variants: { variant: { default: "", outline: "border-2", ghost: "hover:bg-accent hover:text-accent-foreground", }, size: { default: "h-10 px-4 py-2", sm: "h-8 px-3 text-sm", lg: "h-12 px-6 text-base", icon: "h-10 w-10", }, state: { default: "", error: "border-destructive focus:ring-destructive", success: "border-success focus:ring-success", warning: "border-warning focus:ring-warning", }, }, defaultVariants: { variant: "default", size: "default", state: "default", }, } ); export interface DatePickerProps extends Omit, "onChange" | "value">, VariantProps { /** * Selected date value */ value?: Date; /** * Callback when date changes */ onChange?: (date: Date | undefined) => void; /** * Placeholder text */ placeholder?: string; /** * Date format string */ formatString?: string; /** * Minimum selectable date */ minDate?: Date; /** * Maximum selectable date */ maxDate?: Date; /** * Disabled dates */ disabledDates?: Date[]; /** * Disabled days of week (0-6, Sunday is 0) */ disabledDaysOfWeek?: number[]; /** * Show week numbers */ showWeekNumbers?: boolean; /** * Icon position */ iconPosition?: "left" | "right"; /** * Custom icon */ icon?: React.ReactNode; /** * Allow clear */ allowClear?: boolean; /** * Read only */ readOnly?: boolean; /** * Show today button */ showTodayButton?: boolean; /** * Calendar props */ calendarProps?: React.ComponentProps; } export function DatePicker({ className, variant, size, state, value, onChange, placeholder = "Pick a date", formatString = "PPP", minDate, maxDate, disabledDates, disabledDaysOfWeek, showWeekNumbers, iconPosition = "left", icon, allowClear = true, readOnly = false, showTodayButton = true, calendarProps, disabled, ...props }: DatePickerProps) { const [open, setOpen] = React.useState(false); const [internalDate, setInternalDate] = React.useState(value); React.useEffect(() => { setInternalDate(value); }, [value]); const handleSelect = (date: Date | Date[] | { from?: Date; to?: Date } | undefined) => { // Handle single date selection for now const singleDate = Array.isArray(date) ? date[0] : (date && typeof date === 'object' && 'from' in date) ? date.from : date as Date | undefined; setInternalDate(singleDate); onChange?.(singleDate); if (singleDate) { setOpen(false); } }; const handleClear = (e: React.MouseEvent) => { e.stopPropagation(); handleSelect(undefined); }; const handleToday = () => { const today = new Date(); handleSelect(today); }; const isDateDisabled = (date: Date) => { if (minDate && date < minDate) return true; if (maxDate && date > maxDate) return true; if (disabledDates?.some(d => d.toDateString() === date.toDateString())) return true; if (disabledDaysOfWeek?.includes(date.getDay())) return true; return false; }; const displayValue = internalDate && isValid(internalDate) ? format(internalDate, formatString) : null; const iconElement = icon || ; return (
{showTodayButton && (
)}
); } /** * DateRangePicker Component * * Select a date range with start and end dates */ export interface DateRangePickerProps extends Omit { /** * Selected date range */ value?: { from: Date | undefined; to: Date | undefined }; /** * Callback when date range changes */ onChange?: (range: { from: Date | undefined; to: Date | undefined } | undefined) => void; /** * Separator between dates */ separator?: string; } export function DateRangePicker({ className, value, onChange, placeholder = "Pick a date range", formatString = "LLL dd, y", separator = " - ", ...props }: DateRangePickerProps) { const [open, setOpen] = React.useState(false); const [internalRange, setInternalRange] = React.useState(value); React.useEffect(() => { setInternalRange(value); }, [value]); const handleSelect = (range: { from: Date | undefined; to: Date | undefined } | undefined) => { setInternalRange(range); onChange?.(range); if (range?.from && range?.to) { setOpen(false); } }; const displayValue = React.useMemo(() => { if (!internalRange?.from) return null; if (!internalRange?.to) { return format(internalRange.from, formatString); } return `${format(internalRange.from, formatString)}${separator}${format( internalRange.to, formatString )}`; }, [internalRange, formatString, separator]); return ( ); } /** * DateTimePicker Component * * Combined date and time picker */ export interface DateTimePickerProps extends DatePickerProps { /** * Show time picker */ showTimePicker?: boolean; /** * Time format (12 or 24 hour) */ timeFormat?: "12" | "24"; /** * Time interval in minutes */ timeInterval?: number; } export function DateTimePicker({ value, onChange, formatString = "PPP p", showTimePicker = true, timeFormat = "24", timeInterval = 15, ...props }: DateTimePickerProps) { const [date, setDate] = React.useState(value); const [time, setTime] = React.useState( value ? format(value, timeFormat === "24" ? "HH:mm" : "hh:mm a") : "00:00" ); const handleDateChange = (newDate: Date | undefined) => { if (!newDate) { setDate(undefined); onChange?.(undefined); return; } const [hours, minutes] = time.split(":").map(Number); newDate.setHours(hours, minutes); setDate(newDate); onChange?.(newDate); }; const handleTimeChange = (e: React.ChangeEvent) => { const newTime = e.target.value; setTime(newTime); if (date) { const [hours, minutes] = newTime.split(":").map(Number); const newDate = new Date(date); newDate.setHours(hours, minutes); setDate(newDate); onChange?.(newDate); } }; // Generate time options const timeOptions = React.useMemo(() => { const options = []; for (let h = 0; h < 24; h++) { for (let m = 0; m < 60; m += timeInterval) { const hour24 = h.toString().padStart(2, "0"); const minute = m.toString().padStart(2, "0"); const time24 = `${hour24}:${minute}`; if (timeFormat === "12") { const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h; const ampm = h < 12 ? "AM" : "PM"; const time12 = `${hour12}:${minute} ${ampm}`; options.push({ value: time24, label: time12 }); } else { options.push({ value: time24, label: time24 }); } } } return options; }, [timeFormat, timeInterval]); return (
{showTimePicker && date && ( )}
); } /** * MonthPicker Component * * Select month and year */ export interface MonthPickerProps extends Omit { /** * Format for displaying the month */ formatString?: string; } export function MonthPicker({ value, onChange, placeholder = "Pick a month", formatString = "MMMM yyyy", ...props }: MonthPickerProps) { const [open, setOpen] = React.useState(false); const [viewDate, setViewDate] = React.useState(value || new Date()); const months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; const handleMonthSelect = (monthIndex: number) => { const newDate = new Date(viewDate); newDate.setMonth(monthIndex); onChange?.(newDate); setOpen(false); }; const handleYearChange = (increment: number) => { const newDate = new Date(viewDate); newDate.setFullYear(newDate.getFullYear() + increment); setViewDate(newDate); }; const displayValue = value ? format(value, formatString) : null; return (
{viewDate.getFullYear()}
{months.map((month, index) => ( ))}
); } // Re-export utilities export { format } from "date-fns"; // Import ChevronLeft and ChevronRight for MonthPicker import { ChevronLeft, ChevronRight } from "lucide-react";