diff --git a/src/components/ui/calendar-custom-caption.tsx b/src/components/ui/calendar-custom-caption.tsx new file mode 100644 index 00000000..f5ff1d2d --- /dev/null +++ b/src/components/ui/calendar-custom-caption.tsx @@ -0,0 +1,73 @@ +import * as React from "react"; +import { CaptionProps, useNavigation } from "react-day-picker"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { YearGridSelector } from "@/components/ui/year-grid-selector"; +import { cn } from "@/lib/utils"; + +interface CustomCaptionProps extends CaptionProps { + fromYear?: number; + toYear?: number; +} + +export function CustomCalendarCaption({ + displayMonth, + fromYear = 1800, + toYear = new Date().getFullYear() + 10, +}: CustomCaptionProps) { + const { goToMonth, nextMonth, previousMonth, currentMonth } = useNavigation(); + + const handleYearSelect = (year: number) => { + const newDate = new Date(displayMonth); + newDate.setFullYear(year); + goToMonth(newDate); + }; + + const monthName = displayMonth.toLocaleString("en-US", { month: "long" }); + const selectedYear = displayMonth.getFullYear(); + + return ( +
+ + +
+ {monthName} + + + +
+ + +
+ ); +} diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index bff47f58..236ff114 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -4,14 +4,18 @@ import { DayPicker } from "react-day-picker"; import { cn } from "@/lib/utils"; import { buttonVariants } from "@/components/ui/button"; +import { CustomCalendarCaption } from "@/components/ui/calendar-custom-caption"; -export type CalendarProps = React.ComponentProps; +export type CalendarProps = React.ComponentProps & { + enableYearGrid?: boolean; +}; -function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { +function Calendar({ className, classNames, showOutsideDays = true, enableYearGrid = false, ...props }: CalendarProps) { + const captionLayout = enableYearGrid ? undefined : (props.captionLayout || "dropdown-buttons"); return ( ( + + ) + : undefined, IconLeft: ({ ..._props }) => , IconRight: ({ ..._props }) => , }} diff --git a/src/components/ui/date-picker.tsx b/src/components/ui/date-picker.tsx index abd7bac9..41438195 100644 --- a/src/components/ui/date-picker.tsx +++ b/src/components/ui/date-picker.tsx @@ -24,6 +24,7 @@ export interface DatePickerProps { toYear?: number; allowTextEntry?: boolean; dateFormat?: string; + enableYearGrid?: boolean; } export function DatePicker({ @@ -38,6 +39,7 @@ export function DatePicker({ toYear, allowTextEntry = false, dateFormat = "yyyy-MM-dd", + enableYearGrid = false, }: DatePickerProps) { const [open, setOpen] = React.useState(false); const [textInput, setTextInput] = React.useState(""); @@ -139,7 +141,7 @@ export function DatePicker({ className="p-3 pointer-events-auto" fromYear={fromYear} toYear={toYear} - captionLayout="dropdown-buttons" + enableYearGrid={enableYearGrid} /> @@ -176,7 +178,7 @@ export function DatePicker({ className="p-3 pointer-events-auto" fromYear={fromYear} toYear={toYear} - captionLayout="dropdown-buttons" + enableYearGrid={enableYearGrid} /> diff --git a/src/components/ui/year-grid-selector.tsx b/src/components/ui/year-grid-selector.tsx new file mode 100644 index 00000000..5f85ecfb --- /dev/null +++ b/src/components/ui/year-grid-selector.tsx @@ -0,0 +1,176 @@ +import * as React from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +interface YearGridSelectorProps { + selectedYear: number; + onYearSelect: (year: number) => void; + fromYear?: number; + toYear?: number; + children: React.ReactNode; +} + +export function YearGridSelector({ + selectedYear, + onYearSelect, + fromYear = 1800, + toYear = new Date().getFullYear() + 10, + children, +}: YearGridSelectorProps) { + const [open, setOpen] = React.useState(false); + const [view, setView] = React.useState<"year" | "decade">("year"); + const [startYear, setStartYear] = React.useState(() => { + // Start from the decade containing the selected year + return Math.floor(selectedYear / 10) * 10; + }); + + const handleYearSelect = (year: number) => { + onYearSelect(year); + setOpen(false); + }; + + const handleDecadeSelect = (decade: number) => { + setStartYear(decade); + setView("year"); + }; + + const navigateYears = (direction: "prev" | "next") => { + setStartYear((prev) => prev + (direction === "next" ? 12 : -12)); + }; + + const navigateDecades = (direction: "prev" | "next") => { + setStartYear((prev) => prev + (direction === "next" ? 120 : -120)); + }; + + const renderYearGrid = () => { + const years: number[] = []; + for (let i = 0; i < 12; i++) { + const year = startYear + i; + if (year >= fromYear && year <= toYear) { + years.push(year); + } + } + + const endYear = startYear + 11; + const currentYear = new Date().getFullYear(); + + return ( +
+
+ + + +
+
+ {years.map((year) => ( + + ))} +
+
+ ); + }; + + const renderDecadeGrid = () => { + const decades: number[] = []; + const decadeStart = Math.floor(startYear / 100) * 100; + for (let i = 0; i < 12; i++) { + const decade = decadeStart + i * 10; + if (decade >= Math.floor(fromYear / 10) * 10 && decade <= Math.ceil(toYear / 10) * 10) { + decades.push(decade); + } + } + + const decadeEnd = decadeStart + 110; + + return ( +
+
+ + + +
+
+ {decades.map((decade) => ( + + ))} +
+
+ ); + }; + + return ( + + {children} + + {view === "year" ? renderYearGrid() : renderDecadeGrid()} + + + ); +}