From c59ab9523f36465669926b0185f94f3bda8a6175 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sat, 11 Oct 2025 17:50:46 +0000
Subject: [PATCH] feat: Implement year grid navigation
---
src/components/ui/calendar-custom-caption.tsx | 73 ++++++++
src/components/ui/calendar.tsx | 19 +-
src/components/ui/date-picker.tsx | 6 +-
src/components/ui/year-grid-selector.tsx | 176 ++++++++++++++++++
4 files changed, 269 insertions(+), 5 deletions(-)
create mode 100644 src/components/ui/calendar-custom-caption.tsx
create mode 100644 src/components/ui/year-grid-selector.tsx
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()}
+
+
+ );
+}