Skip to content

Instantly share code, notes, and snippets.

@kev1n
Last active January 28, 2025 05:35
Show Gist options
  • Save kev1n/54c3c43895e6dc5115c4b258bcab1271 to your computer and use it in GitHub Desktop.
Save kev1n/54c3c43895e6dc5115c4b258bcab1271 to your computer and use it in GitHub Desktop.
Better Shadcn Date Range Selector
"use client";
import * as React from "react";
import { CalendarIcon } from "@radix-ui/react-icons";
import { format, isAfter, isBefore } from "date-fns";
import { cn } from "~/lib/utils";
import { Button } from "~/app/_components/shadcn-ui/button";
import { Calendar } from "~/app/_components/shadcn-ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/app/_components/shadcn-ui/popover";
// MODIFIED FROM https://ui.shadcn.com/docs/components/date-picker
export function DatePickerWithRange({
className,
field,
}: {
className?: string;
field: {
onChange: (dates: {
checkIn: Date | undefined;
checkOut: Date | undefined;
}) => void;
value:
| { checkIn: Date | undefined; checkOut: Date | undefined }
| undefined;
};
}) {
const [isOpen, setIsOpen] = React.useState(false);
const [activeField, setActiveField] = React.useState<
"checkIn" | "checkOut" | null
>(null);
const [previewDate, setPreviewDate] = React.useState<Date | null>(null);
const formatDate = (date: Date | undefined) => {
if (!date) return "";
return format(date, "EEE, MM/dd");
};
const handleDateSelect = (date: Date | undefined) => {
if (!date) return;
if (activeField === "checkIn") {
if (field.value?.checkOut && isAfter(date, field.value.checkOut)) {
field.onChange({
checkIn: date,
checkOut: undefined,
});
} else {
field.onChange({
checkIn: date,
checkOut: field.value?.checkOut,
});
}
setActiveField("checkOut");
} else if (activeField === "checkOut") {
// If check-in date exists and new check-out date is before it, reset check-in
if (field.value?.checkIn && isBefore(date, field.value.checkIn)) {
field.onChange({
checkIn: undefined,
checkOut: date,
});
setActiveField("checkIn");
} else {
field.onChange({
checkIn: field.value?.checkIn,
checkOut: date,
});
setIsOpen(false);
setActiveField(null);
}
}
setPreviewDate(null);
};
const getSelectedRange = () => {
// Always show the current range if it exists
const currentRange = {
from: field.value?.checkIn,
to: field.value?.checkOut,
};
// If no preview or no active field, just show current range
if (!previewDate || !activeField) {
return currentRange;
}
// For check-in selection, only show preview if it's before checkout (if exists)
if (activeField === "checkIn") {
if (
!field.value?.checkOut ||
isBefore(previewDate, field.value.checkOut)
) {
return {
from: previewDate,
to: field.value?.checkOut,
};
}
return currentRange;
}
// For check-out selection, only show preview if it's after checkin (if exists)
if (activeField === "checkOut") {
if (!field.value?.checkIn || isAfter(previewDate, field.value.checkIn)) {
return {
from: field.value?.checkIn,
to: previewDate,
};
}
return currentRange;
}
return currentRange;
};
return (
<div className={cn("grid gap-2", className)}>
<div className="flex gap-2">
<Popover
open={isOpen}
onOpenChange={(open) => {
setIsOpen(open);
if (!open) {
setActiveField(null);
setPreviewDate(null);
}
}}
>
<div className="flex w-full min-w-[225px] gap-2">
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full justify-start text-left font-normal",
!field.value?.checkIn && "text-muted-foreground",
activeField === "checkIn" && "ring-2 ring-primary",
)}
onClick={() => {
setActiveField("checkIn");
setIsOpen(true);
}}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{field.value?.checkIn ? (
formatDate(field.value.checkIn)
) : (
<span>Check In Date</span>
)}
</Button>
</PopoverTrigger>
<Button
variant="outline"
className={cn(
"w-full justify-start text-left font-normal",
!field.value?.checkOut && "text-muted-foreground",
activeField === "checkOut" && "ring-2 ring-primary",
)}
onClick={() => {
setActiveField("checkOut");
setIsOpen(true);
}}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{field.value?.checkOut ? (
formatDate(field.value.checkOut)
) : (
<span>Check Out Date</span>
)}
</Button>
</div>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
initialFocus
mode="range"
selected={getSelectedRange()}
onSelect={() => {
if (previewDate) {
handleDateSelect(previewDate);
}
}}
disabled={[
{ before: new Date() },
...(activeField === "checkOut" && field.value?.checkIn
? [{ before: field.value.checkIn }]
: []),
]}
numberOfMonths={2}
onDayMouseEnter={(date) => {
setPreviewDate(date);
}}
onDayMouseLeave={() => {
setPreviewDate(null);
}}
/>
</PopoverContent>
</Popover>
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment