2025-08-04 17:45:44 +02:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { differenceInCalendarDays } from "date-fns";
|
|
|
|
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
type Dispatch,
|
|
|
|
|
type HTMLAttributes,
|
|
|
|
|
type ReactNode,
|
|
|
|
|
type SetStateAction,
|
|
|
|
|
type TableHTMLAttributes,
|
|
|
|
|
useCallback,
|
|
|
|
|
useMemo,
|
|
|
|
|
useState,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "react";
|
2025-08-28 18:27:07 +02:00
|
|
|
import {
|
|
|
|
|
DayPicker,
|
|
|
|
|
type DayPickerProps,
|
|
|
|
|
labelNext,
|
|
|
|
|
labelPrevious,
|
|
|
|
|
useDayPicker,
|
|
|
|
|
} from "react-day-picker";
|
|
|
|
|
import { Button, buttonVariants } from "~/components/ui/button";
|
|
|
|
|
import { cn } from "~/lib/utils";
|
2025-08-04 17:45:44 +02:00
|
|
|
|
|
|
|
|
type CalendarProps = DayPickerProps & {
|
2025-08-28 18:27:07 +02:00
|
|
|
/**
|
|
|
|
|
* In the year view, the number of years to display at once.
|
|
|
|
|
* @default 12
|
|
|
|
|
*/
|
|
|
|
|
yearRange?: number;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
/**
|
|
|
|
|
* Wether to show the year switcher in the caption.
|
|
|
|
|
* @default true
|
|
|
|
|
*/
|
|
|
|
|
showYearSwitcher?: boolean;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
monthsClassName?: string;
|
|
|
|
|
monthCaptionClassName?: string;
|
|
|
|
|
weekdaysClassName?: string;
|
|
|
|
|
weekdayClassName?: string;
|
|
|
|
|
monthClassName?: string;
|
|
|
|
|
captionClassName?: string;
|
|
|
|
|
captionLabelClassName?: string;
|
|
|
|
|
buttonNextClassName?: string;
|
|
|
|
|
buttonPreviousClassName?: string;
|
|
|
|
|
navClassName?: string;
|
|
|
|
|
monthGridClassName?: string;
|
|
|
|
|
weekClassName?: string;
|
|
|
|
|
dayClassName?: string;
|
|
|
|
|
dayButtonClassName?: string;
|
|
|
|
|
rangeStartClassName?: string;
|
|
|
|
|
rangeEndClassName?: string;
|
|
|
|
|
selectedClassName?: string;
|
|
|
|
|
todayClassName?: string;
|
|
|
|
|
outsideClassName?: string;
|
|
|
|
|
disabledClassName?: string;
|
|
|
|
|
rangeMiddleClassName?: string;
|
|
|
|
|
hiddenClassName?: string;
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type NavView = "days" | "years";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A custom calendar component built on top of react-day-picker.
|
|
|
|
|
* @param props The props for the calendar.
|
|
|
|
|
* @default yearRange 12
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function Calendar({
|
2025-08-28 18:27:07 +02:00
|
|
|
className,
|
|
|
|
|
showOutsideDays = true,
|
|
|
|
|
showYearSwitcher = true,
|
|
|
|
|
yearRange = 12,
|
|
|
|
|
numberOfMonths,
|
|
|
|
|
...props
|
2025-08-04 17:45:44 +02:00
|
|
|
}: CalendarProps) {
|
2025-08-28 18:27:07 +02:00
|
|
|
const [navView, setNavView] = useState<NavView>("days");
|
|
|
|
|
const [displayYears, setDisplayYears] = useState<{
|
|
|
|
|
from: number;
|
|
|
|
|
to: number;
|
|
|
|
|
}>(
|
|
|
|
|
useMemo(() => {
|
|
|
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
|
return {
|
|
|
|
|
from: currentYear - Math.floor(yearRange / 2 - 1),
|
|
|
|
|
to: currentYear + Math.ceil(yearRange / 2),
|
|
|
|
|
};
|
|
|
|
|
}, [yearRange]),
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const { onPrevClick, startMonth, endMonth } = props;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const columnsDisplayed = navView === "years" ? 1 : numberOfMonths;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const _monthsClassName = cn("relative flex", props.monthsClassName);
|
|
|
|
|
const _monthCaptionClassName = cn(
|
|
|
|
|
"relative mx-10 flex h-7 items-center justify-center",
|
|
|
|
|
props.monthCaptionClassName,
|
|
|
|
|
);
|
|
|
|
|
const _weekdaysClassName = cn("flex flex-row", props.weekdaysClassName);
|
|
|
|
|
const _weekdayClassName = cn(
|
|
|
|
|
"w-8 text-sm font-normal text-muted-foreground",
|
|
|
|
|
props.weekdayClassName,
|
|
|
|
|
);
|
|
|
|
|
const _monthClassName = cn("w-full", props.monthClassName);
|
|
|
|
|
const _captionClassName = cn(
|
|
|
|
|
"relative flex items-center justify-center pt-1",
|
|
|
|
|
props.captionClassName,
|
|
|
|
|
);
|
|
|
|
|
const _captionLabelClassName = cn(
|
|
|
|
|
"truncate text-sm font-medium",
|
|
|
|
|
props.captionLabelClassName,
|
|
|
|
|
props.captionLayout === "dropdown" &&
|
|
|
|
|
"flex items-center text-sm font-medium border border-neutral-200 rounded-md px-2 py-1",
|
|
|
|
|
);
|
|
|
|
|
const buttonNavClassName = buttonVariants({
|
|
|
|
|
variant: "outline",
|
|
|
|
|
className:
|
|
|
|
|
"absolute size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
|
|
|
});
|
|
|
|
|
const _buttonNextClassName = cn(
|
|
|
|
|
buttonNavClassName,
|
|
|
|
|
"right-0",
|
|
|
|
|
props.buttonNextClassName,
|
|
|
|
|
);
|
|
|
|
|
const _buttonPreviousClassName = cn(
|
|
|
|
|
buttonNavClassName,
|
|
|
|
|
"left-0",
|
|
|
|
|
props.buttonPreviousClassName,
|
|
|
|
|
);
|
|
|
|
|
const _navClassName = cn("flex items-start", props.navClassName);
|
|
|
|
|
const _monthGridClassName = cn("mx-auto mt-4", props.monthGridClassName);
|
|
|
|
|
const _weekClassName = cn("mt-2 flex w-max items-start", props.weekClassName);
|
|
|
|
|
const _dayClassName = cn(
|
|
|
|
|
"flex size-8 flex-1 items-center justify-center p-0 text-sm",
|
|
|
|
|
props.dayClassName,
|
|
|
|
|
);
|
|
|
|
|
const _dayButtonClassName = cn(
|
|
|
|
|
buttonVariants({ variant: "ghost" }),
|
|
|
|
|
"size-8 rounded-md p-0 font-normal transition-none aria-selected:opacity-100",
|
|
|
|
|
props.dayButtonClassName,
|
|
|
|
|
);
|
|
|
|
|
const buttonRangeClassName =
|
|
|
|
|
"bg-accent [&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground";
|
|
|
|
|
const _rangeStartClassName = cn(
|
|
|
|
|
buttonRangeClassName,
|
|
|
|
|
"rounded-s-md",
|
|
|
|
|
props.rangeStartClassName,
|
|
|
|
|
);
|
|
|
|
|
const _rangeEndClassName = cn(
|
|
|
|
|
buttonRangeClassName,
|
|
|
|
|
"rounded-e-md",
|
|
|
|
|
props.rangeEndClassName,
|
|
|
|
|
);
|
|
|
|
|
const _rangeMiddleClassName = cn(
|
|
|
|
|
"bg-accent !text-foreground [&>button]:bg-transparent [&>button]:!text-foreground [&>button]:hover:bg-transparent [&>button]:hover:!text-foreground",
|
|
|
|
|
props.rangeMiddleClassName,
|
|
|
|
|
);
|
|
|
|
|
const _selectedClassName = cn(
|
|
|
|
|
"[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground",
|
|
|
|
|
props.selectedClassName,
|
|
|
|
|
);
|
|
|
|
|
const _todayClassName = cn(
|
|
|
|
|
"[&>button]:bg-accent [&>button]:text-accent-foreground",
|
|
|
|
|
props.todayClassName,
|
|
|
|
|
);
|
|
|
|
|
const _outsideClassName = cn(
|
|
|
|
|
"text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
|
|
|
|
props.outsideClassName,
|
|
|
|
|
);
|
|
|
|
|
const _disabledClassName = cn(
|
|
|
|
|
"text-muted-foreground opacity-50",
|
|
|
|
|
props.disabledClassName,
|
|
|
|
|
);
|
|
|
|
|
const _hiddenClassName = cn("invisible flex-1", props.hiddenClassName);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
|
|
|
|
<DayPicker
|
|
|
|
|
showOutsideDays={showOutsideDays}
|
|
|
|
|
className={cn("p-3", className)}
|
|
|
|
|
style={{
|
|
|
|
|
width: `${248.8 * (columnsDisplayed ?? 1)}px`,
|
|
|
|
|
}}
|
|
|
|
|
classNames={{
|
|
|
|
|
months: _monthsClassName,
|
|
|
|
|
month_caption: _monthCaptionClassName,
|
|
|
|
|
weekdays: _weekdaysClassName,
|
|
|
|
|
weekday: _weekdayClassName,
|
|
|
|
|
month: _monthClassName,
|
|
|
|
|
caption: _captionClassName,
|
|
|
|
|
caption_label: _captionLabelClassName,
|
|
|
|
|
button_next: _buttonNextClassName,
|
|
|
|
|
button_previous: _buttonPreviousClassName,
|
|
|
|
|
nav: _navClassName,
|
|
|
|
|
month_grid: _monthGridClassName,
|
|
|
|
|
week: _weekClassName,
|
|
|
|
|
day: _dayClassName,
|
|
|
|
|
day_button: _dayButtonClassName,
|
|
|
|
|
range_start: _rangeStartClassName,
|
|
|
|
|
range_middle: _rangeMiddleClassName,
|
|
|
|
|
range_end: _rangeEndClassName,
|
|
|
|
|
selected: _selectedClassName,
|
|
|
|
|
today: _todayClassName,
|
|
|
|
|
outside: _outsideClassName,
|
|
|
|
|
disabled: _disabledClassName,
|
|
|
|
|
hidden: _hiddenClassName,
|
|
|
|
|
months_dropdown: "dropdown-override",
|
|
|
|
|
years_dropdown: "dropdown-override",
|
|
|
|
|
}}
|
|
|
|
|
components={{
|
|
|
|
|
Chevron: ({ orientation }) => {
|
|
|
|
|
const Icon = orientation === "left" ? ChevronLeft : ChevronRight;
|
|
|
|
|
return <Icon className="size-4" />;
|
|
|
|
|
},
|
|
|
|
|
Nav: ({ className }) => (
|
|
|
|
|
<Nav
|
|
|
|
|
className={className}
|
|
|
|
|
displayYears={displayYears}
|
|
|
|
|
navView={navView}
|
|
|
|
|
setDisplayYears={setDisplayYears}
|
|
|
|
|
startMonth={startMonth}
|
|
|
|
|
endMonth={endMonth}
|
|
|
|
|
onPrevClick={onPrevClick}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
CaptionLabel: (props) => (
|
|
|
|
|
<CaptionLabel
|
|
|
|
|
showYearSwitcher={showYearSwitcher}
|
|
|
|
|
navView={navView}
|
|
|
|
|
setNavView={setNavView}
|
|
|
|
|
displayYears={displayYears}
|
|
|
|
|
{...props}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
MonthGrid: ({ className, children, ...props }) => (
|
|
|
|
|
<MonthGrid
|
|
|
|
|
className={className}
|
|
|
|
|
displayYears={displayYears}
|
|
|
|
|
startMonth={startMonth}
|
|
|
|
|
endMonth={endMonth}
|
|
|
|
|
navView={navView}
|
|
|
|
|
setNavView={setNavView}
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</MonthGrid>
|
|
|
|
|
),
|
|
|
|
|
}}
|
|
|
|
|
numberOfMonths={columnsDisplayed}
|
|
|
|
|
{...props}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|
|
|
|
|
Calendar.displayName = "Calendar";
|
|
|
|
|
|
|
|
|
|
function Nav({
|
2025-08-28 18:27:07 +02:00
|
|
|
className,
|
|
|
|
|
navView,
|
|
|
|
|
startMonth,
|
|
|
|
|
endMonth,
|
|
|
|
|
displayYears,
|
|
|
|
|
setDisplayYears,
|
|
|
|
|
onPrevClick,
|
|
|
|
|
onNextClick,
|
2025-08-04 17:45:44 +02:00
|
|
|
}: {
|
2025-08-28 18:27:07 +02:00
|
|
|
className?: string;
|
|
|
|
|
navView: NavView;
|
|
|
|
|
startMonth?: Date;
|
|
|
|
|
endMonth?: Date;
|
|
|
|
|
displayYears: { from: number; to: number };
|
|
|
|
|
setDisplayYears: Dispatch<SetStateAction<{ from: number; to: number }>>;
|
|
|
|
|
onPrevClick?: (date: Date) => void;
|
|
|
|
|
onNextClick?: (date: Date) => void;
|
2025-08-04 17:45:44 +02:00
|
|
|
}) {
|
2025-08-28 18:27:07 +02:00
|
|
|
const { nextMonth, previousMonth, goToMonth } = useDayPicker();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const isPreviousDisabled = (() => {
|
|
|
|
|
if (navView === "years") {
|
|
|
|
|
return (
|
|
|
|
|
(startMonth &&
|
|
|
|
|
differenceInCalendarDays(
|
|
|
|
|
new Date(displayYears.from - 1, 0, 1),
|
|
|
|
|
startMonth,
|
|
|
|
|
) < 0) ||
|
|
|
|
|
(endMonth &&
|
|
|
|
|
differenceInCalendarDays(
|
|
|
|
|
new Date(displayYears.from - 1, 0, 1),
|
|
|
|
|
endMonth,
|
|
|
|
|
) > 0)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return !previousMonth;
|
|
|
|
|
})();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const isNextDisabled = (() => {
|
|
|
|
|
if (navView === "years") {
|
|
|
|
|
return (
|
|
|
|
|
(startMonth &&
|
|
|
|
|
differenceInCalendarDays(
|
|
|
|
|
new Date(displayYears.to + 1, 0, 1),
|
|
|
|
|
startMonth,
|
|
|
|
|
) < 0) ||
|
|
|
|
|
(endMonth &&
|
|
|
|
|
differenceInCalendarDays(
|
|
|
|
|
new Date(displayYears.to + 1, 0, 1),
|
|
|
|
|
endMonth,
|
|
|
|
|
) > 0)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return !nextMonth;
|
|
|
|
|
})();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const handlePreviousClick = useCallback(() => {
|
|
|
|
|
if (!previousMonth) return;
|
|
|
|
|
if (navView === "years") {
|
|
|
|
|
setDisplayYears((prev) => ({
|
|
|
|
|
from: prev.from - (prev.to - prev.from + 1),
|
|
|
|
|
to: prev.to - (prev.to - prev.from + 1),
|
|
|
|
|
}));
|
|
|
|
|
onPrevClick?.(
|
|
|
|
|
new Date(
|
|
|
|
|
displayYears.from - (displayYears.to - displayYears.from),
|
|
|
|
|
0,
|
|
|
|
|
1,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
goToMonth(previousMonth);
|
|
|
|
|
onPrevClick?.(previousMonth);
|
|
|
|
|
}, [previousMonth, goToMonth]);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const handleNextClick = useCallback(() => {
|
|
|
|
|
if (!nextMonth) return;
|
|
|
|
|
if (navView === "years") {
|
|
|
|
|
setDisplayYears((prev) => ({
|
|
|
|
|
from: prev.from + (prev.to - prev.from + 1),
|
|
|
|
|
to: prev.to + (prev.to - prev.from + 1),
|
|
|
|
|
}));
|
|
|
|
|
onNextClick?.(
|
|
|
|
|
new Date(
|
|
|
|
|
displayYears.from + (displayYears.to - displayYears.from),
|
|
|
|
|
0,
|
|
|
|
|
1,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
goToMonth(nextMonth);
|
|
|
|
|
onNextClick?.(nextMonth);
|
|
|
|
|
}, [goToMonth, nextMonth]);
|
|
|
|
|
return (
|
|
|
|
|
<nav className={cn("flex items-center", className)}>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="absolute left-0 size-7 bg-transparent p-0 opacity-80 hover:opacity-100"
|
|
|
|
|
type="button"
|
|
|
|
|
tabIndex={isPreviousDisabled ? undefined : -1}
|
|
|
|
|
disabled={isPreviousDisabled}
|
|
|
|
|
aria-label={
|
|
|
|
|
navView === "years"
|
|
|
|
|
? `Go to the previous ${
|
|
|
|
|
displayYears.to - displayYears.from + 1
|
|
|
|
|
} years`
|
|
|
|
|
: labelPrevious(previousMonth)
|
|
|
|
|
}
|
|
|
|
|
onClick={handlePreviousClick}
|
|
|
|
|
>
|
|
|
|
|
<ChevronLeft className="size-4" />
|
|
|
|
|
</Button>
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="absolute right-0 size-7 bg-transparent p-0 opacity-80 hover:opacity-100"
|
|
|
|
|
type="button"
|
|
|
|
|
tabIndex={isNextDisabled ? undefined : -1}
|
|
|
|
|
disabled={isNextDisabled}
|
|
|
|
|
aria-label={
|
|
|
|
|
navView === "years"
|
|
|
|
|
? `Go to the next ${displayYears.to - displayYears.from + 1} years`
|
|
|
|
|
: labelNext(nextMonth)
|
|
|
|
|
}
|
|
|
|
|
onClick={handleNextClick}
|
|
|
|
|
>
|
|
|
|
|
<ChevronRight className="size-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</nav>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function CaptionLabel({
|
2025-08-28 18:27:07 +02:00
|
|
|
children,
|
|
|
|
|
showYearSwitcher,
|
|
|
|
|
navView,
|
|
|
|
|
setNavView,
|
|
|
|
|
displayYears,
|
|
|
|
|
...props
|
2025-08-04 17:45:44 +02:00
|
|
|
}: {
|
2025-08-28 18:27:07 +02:00
|
|
|
showYearSwitcher?: boolean;
|
|
|
|
|
navView: NavView;
|
|
|
|
|
setNavView: Dispatch<SetStateAction<NavView>>;
|
|
|
|
|
displayYears: { from: number; to: number };
|
2025-08-04 17:45:44 +02:00
|
|
|
} & HTMLAttributes<HTMLSpanElement>) {
|
2025-08-28 18:27:07 +02:00
|
|
|
if (!showYearSwitcher) return <span {...props}>{children}</span>;
|
|
|
|
|
return (
|
|
|
|
|
<Button
|
|
|
|
|
className="h-7 w-full truncate text-sm font-medium"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setNavView((prev) => (prev === "days" ? "years" : "days"))}
|
|
|
|
|
>
|
|
|
|
|
{navView === "days"
|
|
|
|
|
? children
|
|
|
|
|
: `${displayYears.from} - ${displayYears.to}`}
|
|
|
|
|
</Button>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function MonthGrid({
|
2025-08-28 18:27:07 +02:00
|
|
|
className,
|
|
|
|
|
children,
|
|
|
|
|
displayYears,
|
|
|
|
|
startMonth,
|
|
|
|
|
endMonth,
|
|
|
|
|
navView,
|
|
|
|
|
setNavView,
|
|
|
|
|
...props
|
2025-08-04 17:45:44 +02:00
|
|
|
}: {
|
2025-08-28 18:27:07 +02:00
|
|
|
className?: string;
|
|
|
|
|
children: ReactNode;
|
|
|
|
|
displayYears: { from: number; to: number };
|
|
|
|
|
startMonth?: Date;
|
|
|
|
|
endMonth?: Date;
|
|
|
|
|
navView: NavView;
|
|
|
|
|
setNavView: Dispatch<SetStateAction<NavView>>;
|
2025-08-04 17:45:44 +02:00
|
|
|
} & TableHTMLAttributes<HTMLTableElement>) {
|
2025-08-28 18:27:07 +02:00
|
|
|
if (navView === "years") {
|
|
|
|
|
return (
|
|
|
|
|
<YearGrid
|
|
|
|
|
displayYears={displayYears}
|
|
|
|
|
startMonth={startMonth}
|
|
|
|
|
endMonth={endMonth}
|
|
|
|
|
setNavView={setNavView}
|
|
|
|
|
navView={navView}
|
|
|
|
|
className={className}
|
|
|
|
|
{...props}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<table className={className} {...props}>
|
|
|
|
|
{children}
|
|
|
|
|
</table>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function YearGrid({
|
2025-08-28 18:27:07 +02:00
|
|
|
className,
|
|
|
|
|
displayYears,
|
|
|
|
|
startMonth,
|
|
|
|
|
endMonth,
|
|
|
|
|
setNavView,
|
|
|
|
|
navView,
|
|
|
|
|
...props
|
2025-08-04 17:45:44 +02:00
|
|
|
}: {
|
2025-08-28 18:27:07 +02:00
|
|
|
className?: string;
|
|
|
|
|
displayYears: { from: number; to: number };
|
|
|
|
|
startMonth?: Date;
|
|
|
|
|
endMonth?: Date;
|
|
|
|
|
setNavView: Dispatch<SetStateAction<NavView>>;
|
|
|
|
|
navView: NavView;
|
2025-08-04 17:45:44 +02:00
|
|
|
} & HTMLAttributes<HTMLDivElement>) {
|
2025-08-28 18:27:07 +02:00
|
|
|
const { goToMonth, selected } = useDayPicker();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
|
|
|
|
<div className={cn("grid grid-cols-4 gap-y-2", className)} {...props}>
|
|
|
|
|
{Array.from(
|
|
|
|
|
{ length: displayYears.to - displayYears.from + 1 },
|
|
|
|
|
(_, i) => {
|
|
|
|
|
const isBefore =
|
|
|
|
|
differenceInCalendarDays(
|
|
|
|
|
new Date(displayYears.from + i, 11, 31),
|
|
|
|
|
// biome-ignore lint/style/noNonNullAssertion: <exists>
|
|
|
|
|
startMonth!,
|
|
|
|
|
) < 0;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const isAfter =
|
|
|
|
|
differenceInCalendarDays(
|
|
|
|
|
new Date(displayYears.from + i, 0, 0),
|
|
|
|
|
// biome-ignore lint/style/noNonNullAssertion: <exists>
|
|
|
|
|
endMonth!,
|
|
|
|
|
) > 0;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const isDisabled = isBefore || isAfter;
|
|
|
|
|
return (
|
|
|
|
|
<Button
|
|
|
|
|
key={i}
|
|
|
|
|
className={cn(
|
|
|
|
|
"text-foreground h-7 w-full text-sm font-normal",
|
|
|
|
|
displayYears.from + i === new Date().getFullYear() &&
|
|
|
|
|
"bg-accent text-accent-foreground font-medium",
|
|
|
|
|
)}
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setNavView("days");
|
|
|
|
|
goToMonth(
|
|
|
|
|
new Date(
|
|
|
|
|
displayYears.from + i,
|
|
|
|
|
(selected as Date | undefined)?.getMonth() ?? 0,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
disabled={navView === "years" ? isDisabled : undefined}
|
|
|
|
|
>
|
|
|
|
|
{displayYears.from + i}
|
|
|
|
|
</Button>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export { Calendar };
|