Skip to content

Instantly share code, notes, and snippets.

@Mr-Vipi
Last active March 4, 2025 08:14
Show Gist options
  • Save Mr-Vipi/39a11119b8c37696650c2dd22021309d to your computer and use it in GitHub Desktop.
Save Mr-Vipi/39a11119b8c37696650c2dd22021309d to your computer and use it in GitHub Desktop.
My custom shadcn calendar to include selection of month and year with react-day-picker-v.9
"use client";
import { ChevronLeft, ChevronRight } from "lucide-react";
import * as React from "react";
import {
Chevron as ChevronDayPicker,
DayPicker,
Dropdown as DropDownDayPicker,
} from "react-day-picker";
import { buttonVariants } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
export type CalendarProps = React.ComponentProps<typeof DayPicker> & {
captionLabelClassName?: string;
dayClassName?: string;
dayButtonClassName?: string;
dropdownsClassName?: string;
footerClassName?: string;
monthClassName?: string;
monthCaptionClassName?: string;
monthGridClassName?: string;
monthsClassName?: string;
navClassName?: string;
buttonNextClassName?: string;
buttonPreviousClassName?: string;
weekClassName?: string;
weekdayClassName?: string;
weekdaysClassName?: string;
rangeEndClassName?: string;
rangeMiddleClassName?: string;
rangeStartClassName?: string;
selectedClassName?: string;
disabledClassName?: string;
hiddenClassName?: string;
outsideClassName?: string;
todayClassName?: string;
selectTriggerClassName?: string;
};
function Calendar({
className,
classNames,
hideNavigation,
showOutsideDays = true,
components,
...props
}: CalendarProps) {
const _monthsClassName = cn(
"relative flex flex-col gap-4 sm:flex-row",
props.monthsClassName
);
const _monthCaptionClassName = cn(
"relative flex h-7 items-center justify-center",
props.monthCaptionClassName
);
const _dropdownsClassName = cn(
"flex items-center justify-center gap-2",
hideNavigation ? "w-full" : "",
props.dropdownsClassName
);
const _footerClassName = cn("pt-3 text-sm", props.footerClassName);
const _weekdaysClassName = cn("flex", props.weekdaysClassName);
const _weekdayClassName = cn(
"w-9 text-sm font-normal text-muted-foreground",
props.weekdayClassName
);
const _captionLabelClassName = cn(
"truncate text-sm font-medium",
props.captionLabelClassName
);
const buttonNavClassName = buttonVariants({
variant: "outline",
className: "z-10 size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
});
const _buttonNextClassName = cn(
buttonNavClassName,
props.buttonNextClassName
);
const _buttonPreviousClassName = cn(
buttonNavClassName,
props.buttonPreviousClassName
);
const _navClassName = cn(
"absolute flex w-full items-center justify-between",
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-9 flex-1 items-center justify-center p-0 text-sm",
props.dayClassName
);
const _dayButtonClassName = cn(
buttonVariants({ variant: "ghost" }),
"size-9 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);
const Chevron = React.useCallback(
({
orientation,
...props
}: React.ComponentProps<typeof ChevronDayPicker>) => {
const Icon = orientation === "left" ? ChevronLeft : ChevronRight;
return <Icon className="size-4" {...props} />;
},
[]
);
const Dropdown = React.useCallback(
({
value,
onChange,
options,
}: React.ComponentProps<typeof DropDownDayPicker>) => {
const selected = options?.find((option) => option.value === value);
const handleChange = (value: string) => {
const changeEvent = {
target: { value },
} as React.ChangeEvent<HTMLSelectElement>;
onChange?.(changeEvent);
};
return (
<Select
value={value?.toString()}
onValueChange={(value) => {
handleChange(value);
}}
>
<SelectTrigger
className={cn(
hideNavigation ? "" : "h-7",
props.selectTriggerClassName
)}
>
<SelectValue>{selected?.label}</SelectValue>
</SelectTrigger>
<SelectContent position="popper" align="center">
<ScrollArea className="h-80">
{options?.map(({ value, label, disabled }, id) => (
<SelectItem
key={`${value}-${id}`}
value={value?.toString()}
disabled={disabled}
>
{label}
</SelectItem>
))}
</ScrollArea>
</SelectContent>
</Select>
);
},
[hideNavigation, props.selectTriggerClassName]
);
return (
<DayPicker
showOutsideDays={showOutsideDays}
hideNavigation={hideNavigation}
className={cn("p-3", className)}
classNames={{
caption_label: _captionLabelClassName,
day: _dayClassName,
day_button: _dayButtonClassName,
dropdowns: _dropdownsClassName,
footer: _footerClassName,
month: props.monthClassName,
month_caption: _monthCaptionClassName,
month_grid: _monthGridClassName,
months: _monthsClassName,
nav: _navClassName,
button_next: _buttonNextClassName,
button_previous: _buttonPreviousClassName,
week: _weekClassName,
weekday: _weekdayClassName,
weekdays: _weekdaysClassName,
range_end: _rangeEndClassName,
range_middle: _rangeMiddleClassName,
range_start: _rangeStartClassName,
selected: _selectedClassName,
disabled: _disabledClassName,
hidden: _hiddenClassName,
outside: _outsideClassName,
today: _todayClassName,
...classNames,
}}
components={{
Chevron,
Dropdown,
...components,
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
export { Calendar };
"use client";
import {
addDays,
addMonths,
format,
isBefore,
isEqual,
isWithinInterval,
} from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import {
Dispatch,
memo,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { DateRange } from "react-day-picker";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
// import { FieldType } from "./booking-form";
type DateRangePickerProps = Readonly<{
date?: DateRange;
setDate?: Dispatch<SetStateAction<DateRange | undefined>>;
// field?: FieldType;
focusedField?: string;
setFocusedField?: Dispatch<SetStateAction<string>>;
className?: string;
bookedDates?: DateRange[];
}>;
const fromMonth = new Date();
const toMonth = addMonths(new Date(), 6);
const DateRangePicker = ({
// date,
// setDate,
// field,
className,
bookedDates,
}: DateRangePickerProps) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [minimumDays, setMinimumDays] = useState<DateRange>({
from: undefined,
to: undefined,
});
const [date, setDate] = useState<DateRange | undefined>({
from: undefined,
to: undefined,
});
useEffect(() => {
if (!date) {
return;
}
if (date.from && date.to) {
setIsPopoverOpen(false);
setMinimumDays({
from: undefined,
to: undefined,
});
return;
}
if (date.from && !date.to) {
setMinimumDays({
from: addDays(date.from, 1),
to: addDays(date.from, 1),
});
} else {
setMinimumDays({
from: undefined,
to: undefined,
});
}
}, [date]);
function formatDateRange(from: Date, to: Date | undefined) {
const formattedFrom = format(from, "LLL dd, y");
return to ? `${formattedFrom} - ${format(to, "LLL dd, y")}` : formattedFrom;
}
const getDateText = useCallback(
(/*field: FieldType | undefined,*/ date: DateRange | undefined) => {
// if (field?.value) {
// const { from, to } = field.value;
// return from ? formatDateRange(from, to) : "Departure / Return";
// }
if (date) {
const { from, to } = date;
return from ? formatDateRange(from, to) : "Departure / Return";
}
return "Departure / Return";
},
[]
);
const checkIfBookingAvailable = useCallback(
({ from, to }: DateRange): boolean => {
let isAvailable = true;
if (bookedDates) {
bookedDates.forEach((bookedDate) => {
if (
!(
bookedDate.from &&
bookedDate.to &&
from &&
to &&
isWithinInterval(bookedDate.from, {
start: from,
end: to,
}) &&
isWithinInterval(bookedDate.to, { start: from, end: to })
)
) {
return;
}
isAvailable = false;
});
}
return isAvailable;
},
[bookedDates]
);
const updateDate = useCallback(
(day: Date): void => {
if (setDate) {
setDate((prev) => {
if (prev?.from && isEqual(prev.from, day)) {
return { from: undefined, to: undefined };
}
if (prev?.to) {
return { from: day, to: undefined };
} else if (prev?.from && isBefore(prev?.from, day)) {
return checkIfBookingAvailable({ from: prev?.from, to: day })
? { from: prev?.from, to: day }
: { from: day, to: undefined };
} else {
return { from: day, to: undefined };
}
});
}
// if (!field) {
// return;
// }
// const updateMinimumDaysAndResetToDate = () => {
// setMinimumDays({ from: addDays(day, 1), to: addDays(day, 1) });
// field.onChange({ from: day, to: undefined });
// };
// if (field.value.from && isEqual(field.value.from, day)) {
// field.onChange({ from: undefined, to: undefined });
// setMinimumDays({
// from: undefined,
// to: undefined,
// });
// return;
// }
// if (field.value.to) {
// updateMinimumDaysAndResetToDate();
// } else if (field.value.from && isBefore(field.value.from, day)) {
// if (checkIfBookingAvailable({ from: field.value.from, to: day })) {
// setIsPopoverOpen(false);
// field.onChange({ from: field.value.from, to: day });
// setMinimumDays({ from: undefined, to: undefined });
// } else {
// updateMinimumDaysAndResetToDate();
// }
// } else {
// updateMinimumDaysAndResetToDate();
// }
},
[setDate, /*field,*/ checkIfBookingAvailable]
);
const handleOpenChange = useCallback(
(value: boolean) => {
setIsPopoverOpen(value);
},
[setIsPopoverOpen]
);
const disabled = useMemo(
() => [
{
before: new Date(),
},
minimumDays,
...(bookedDates ?? []),
],
[minimumDays, bookedDates]
);
return (
<Popover open={isPopoverOpen} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full pl-3 text-left",
date?.from /*|| field?.value.from*/ ? "" : "text-muted-foreground",
className
)}
>
<span className="pt-1">{getDateText(/*field,*/ date)}</span>
<CalendarIcon className="ml-auto size-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="center">
<Calendar
autoFocus
mode="range"
defaultMonth={date?.from /*|| field?.value?.from*/}
selected={
date /*|| {
from: field?.value?.from,
to: field?.value?.to,
}*/
}
numberOfMonths={2}
showOutsideDays={false}
startMonth={fromMonth}
endMonth={toMonth}
onSelect={(_date, day) => updateDate(day)}
disabled={disabled}
/>
</PopoverContent>
</Popover>
);
};
const areEqual = (
prevProps: DateRangePickerProps,
nextProps: DateRangePickerProps
) => {
return (
prevProps?.date === nextProps.date &&
// prevProps?.field === nextProps.field &&
prevProps?.focusedField === nextProps.focusedField &&
prevProps?.bookedDates === nextProps.bookedDates &&
prevProps?.className === nextProps.className
);
};
export default memo(DateRangePicker, areEqual);
"use client";
import { format } from "date-fns";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { CalendarIcon } from "lucide-react";
// Interval for the date picker
// const startDate = subYears(new Date(), 75);
// const endDate = subYears(new Date(), 23);
export default function DatePicker() {
const [date, setDate] = React.useState<Date>();
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full pl-3 text-left",
date ? "md:text-sm" : "text-muted-foreground"
)}
>
{date ? format(date, "PPP") : <span>Pick a date</span>}
<CalendarIcon className="ml-auto size-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="center">
<Calendar
mode="single"
selected={date}
onSelect={setDate}
autoFocus
defaultMonth={date}
showOutsideDays={false}
captionLayout="dropdown"
// hideNavigation
// selectTriggerClassName="transition-colors duration-200 ease-in-out hover:border-primary focus:border-primary focus:shadow-around-primary focus:ring-0 focus:ring-offset-0"
// startMonth={startDate}
// endMonth={endDate}
// disabled={[
// { before: startDate },
// { after: endDate },
// ]}
// footer={DatePickerFooter}
/>
</PopoverContent>
</Popover>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment