Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/sersavan/shadcn-calendar-component

A calendar date picker component designed with shadcn/ui
https://github.com/sersavan/shadcn-calendar-component

calendar calendar-component date-picker date-picker-range datepicker nextjs radix-ui reactjs shadcn shadcn-ui

Last synced: 24 days ago
JSON representation

A calendar date picker component designed with shadcn/ui

Awesome Lists containing this project

README

        

## Calendar Date Picker Component in Next.js

### Prerequisites

Ensure you have a Next.js project set up. If not, create one:

```bash
npx create-next-app my-app --typescript
cd my-app
```

### Step 1: Install Required Dependencies

Install the necessary dependencies:

```bash
npm install date-fns date-fns-tz react-day-picker
npx shadcn@latest init
npx shadcn@latest add button calendar popover select
```

### Step 2: Create the Calendar Date Picker Component

Create `calendar-date-picker.tsx` in your `components` directory:

```tsx
// src/components/calendar-date-picker.tsx

"use client";

import * as React from "react";
import { CalendarIcon } from "lucide-react";
import {
startOfWeek,
endOfWeek,
subDays,
startOfMonth,
endOfMonth,
startOfYear,
endOfYear,
startOfDay,
endOfDay,
} from "date-fns";
import { toDate, formatInTimeZone } from "date-fns-tz";
import { DateRange } from "react-day-picker";
import { cva, VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";

const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];

const multiSelectVariants = cva(
"flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium text-foreground ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground text-background",
link: "text-primary underline-offset-4 hover:underline text-background",
},
},
defaultVariants: {
variant: "default",
},
}
);

interface CalendarDatePickerProps
extends React.HTMLAttributes,
VariantProps {
id?: string;
className?: string;
date: DateRange;
closeOnSelect?: boolean;
numberOfMonths?: 1 | 2;
yearsRange?: number;
onDateSelect: (range: { from: Date; to: Date }) => void;
}

export const CalendarDatePicker = React.forwardRef<
HTMLButtonElement,
CalendarDatePickerProps
>(
(
{
id = "calendar-date-picker",
className,
date,
closeOnSelect = false,
numberOfMonths = 2,
yearsRange = 10,
onDateSelect,
variant,
...props
},
ref
) => {
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [selectedRange, setSelectedRange] = React.useState(
numberOfMonths === 2 ? "This Year" : "Today"
);
const [monthFrom, setMonthFrom] = React.useState(
date?.from
);
const [yearFrom, setYearFrom] = React.useState(
date?.from?.getFullYear()
);
const [monthTo, setMonthTo] = React.useState(
numberOfMonths === 2 ? date?.to : date?.from
);
const [yearTo, setYearTo] = React.useState(
numberOfMonths === 2 ? date?.to?.getFullYear() : date?.from?.getFullYear()
);
const [highlightedPart, setHighlightedPart] = React.useState(
null
);

const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const handleClose = () => setIsPopoverOpen(false);

const handleTogglePopover = () => setIsPopoverOpen((prev) => !prev);

const selectDateRange = (from: Date, to: Date, range: string) => {
const startDate = startOfDay(toDate(from, { timeZone }));
const endDate =
numberOfMonths === 2 ? endOfDay(toDate(to, { timeZone })) : startDate;
onDateSelect({ from: startDate, to: endDate });
setSelectedRange(range);
setMonthFrom(from);
setYearFrom(from.getFullYear());
setMonthTo(to);
setYearTo(to.getFullYear());
closeOnSelect && setIsPopoverOpen(false);
};

const handleDateSelect = (range: DateRange | undefined) => {
if (range) {
let from = startOfDay(toDate(range.from as Date, { timeZone }));
let to = range.to ? endOfDay(toDate(range.to, { timeZone })) : from;
if (numberOfMonths === 1) {
if (range.from !== date.from) {
to = from;
} else {
from = startOfDay(toDate(range.to as Date, { timeZone }));
}
}
onDateSelect({ from, to });
setMonthFrom(from);
setYearFrom(from.getFullYear());
setMonthTo(to);
setYearTo(to.getFullYear());
}
setSelectedRange(null);
};

const handleMonthChange = (newMonthIndex: number, part: string) => {
setSelectedRange(null);
if (part === "from") {
if (yearFrom !== undefined) {
if (newMonthIndex < 0 || newMonthIndex > yearsRange + 1) return;
const newMonth = new Date(yearFrom, newMonthIndex, 1);
const from =
numberOfMonths === 2
? startOfMonth(toDate(newMonth, { timeZone }))
: date?.from
? new Date(
date.from.getFullYear(),
newMonth.getMonth(),
date.from.getDate()
)
: newMonth;
const to =
numberOfMonths === 2
? date.to
? endOfDay(toDate(date.to, { timeZone }))
: endOfMonth(toDate(newMonth, { timeZone }))
: from;
if (from <= to) {
onDateSelect({ from, to });
setMonthFrom(newMonth);
setMonthTo(date.to);
}
}
} else {
if (yearTo !== undefined) {
if (newMonthIndex < 0 || newMonthIndex > yearsRange + 1) return;
const newMonth = new Date(yearTo, newMonthIndex, 1);
const from = date.from
? startOfDay(toDate(date.from, { timeZone }))
: startOfMonth(toDate(newMonth, { timeZone }));
const to =
numberOfMonths === 2
? endOfMonth(toDate(newMonth, { timeZone }))
: from;
if (from <= to) {
onDateSelect({ from, to });
setMonthTo(newMonth);
setMonthFrom(date.from);
}
}
}
};

const handleYearChange = (newYear: number, part: string) => {
setSelectedRange(null);
if (part === "from") {
if (years.includes(newYear)) {
const newMonth = monthFrom
? new Date(newYear, monthFrom ? monthFrom.getMonth() : 0, 1)
: new Date(newYear, 0, 1);
const from =
numberOfMonths === 2
? startOfMonth(toDate(newMonth, { timeZone }))
: date.from
? new Date(newYear, newMonth.getMonth(), date.from.getDate())
: newMonth;
const to =
numberOfMonths === 2
? date.to
? endOfDay(toDate(date.to, { timeZone }))
: endOfMonth(toDate(newMonth, { timeZone }))
: from;
if (from <= to) {
onDateSelect({ from, to });
setYearFrom(newYear);
setMonthFrom(newMonth);
setYearTo(date.to?.getFullYear());
setMonthTo(date.to);
}
}
} else {
if (years.includes(newYear)) {
const newMonth = monthTo
? new Date(newYear, monthTo.getMonth(), 1)
: new Date(newYear, 0, 1);
const from = date.from
? startOfDay(toDate(date.from, { timeZone }))
: startOfMonth(toDate(newMonth, { timeZone }));
const to =
numberOfMonths === 2
? endOfMonth(toDate(newMonth, { timeZone }))
: from;
if (from <= to) {
onDateSelect({ from, to });
setYearTo(newYear);
setMonthTo(newMonth);
setYearFrom(date.from?.getFullYear());
setMonthFrom(date.from);
}
}
}
};

const today = new Date();

const years = Array.from(
{ length: yearsRange + 1 },
(_, i) => today.getFullYear() - yearsRange / 2 + i
);

const dateRanges = [
{ label: "Today", start: today, end: today },
{ label: "Yesterday", start: subDays(today, 1), end: subDays(today, 1) },
{
label: "This Week",
start: startOfWeek(today, { weekStartsOn: 1 }),
end: endOfWeek(today, { weekStartsOn: 1 }),
},
{
label: "Last Week",
start: subDays(startOfWeek(today, { weekStartsOn: 1 }), 7),
end: subDays(endOfWeek(today, { weekStartsOn: 1 }), 7),
},
{ label: "Last 7 Days", start: subDays(today, 6), end: today },
{
label: "This Month",
start: startOfMonth(today),
end: endOfMonth(today),
},
{
label: "Last Month",
start: startOfMonth(subDays(today, today.getDate())),
end: endOfMonth(subDays(today, today.getDate())),
},
{ label: "This Year", start: startOfYear(today), end: endOfYear(today) },
{
label: "Last Year",
start: startOfYear(subDays(today, 365)),
end: endOfYear(subDays(today, 365)),
},
];

const handleMouseOver = (part: string) => {
setHighlightedPart(part);
};

const handleMouseLeave = () => {
setHighlightedPart(null);
};

const handleWheel = (event: React.WheelEvent, part: string) => {
event.preventDefault();
setSelectedRange(null);
if (highlightedPart === "firstDay") {
const newDate = new Date(date.from as Date);
const increment = event.deltaY > 0 ? -1 : 1;
newDate.setDate(newDate.getDate() + increment);
if (newDate <= (date.to as Date)) {
numberOfMonths === 2
? onDateSelect({ from: newDate, to: new Date(date.to as Date) })
: onDateSelect({ from: newDate, to: newDate });
setMonthFrom(newDate);
} else if (newDate > (date.to as Date) && numberOfMonths === 1) {
onDateSelect({ from: newDate, to: newDate });
setMonthFrom(newDate);
}
} else if (highlightedPart === "firstMonth") {
const currentMonth = monthFrom ? monthFrom.getMonth() : 0;
const newMonthIndex = currentMonth + (event.deltaY > 0 ? -1 : 1);
handleMonthChange(newMonthIndex, "from");
} else if (highlightedPart === "firstYear" && yearFrom !== undefined) {
const newYear = yearFrom + (event.deltaY > 0 ? -1 : 1);
handleYearChange(newYear, "from");
} else if (highlightedPart === "secondDay") {
const newDate = new Date(date.to as Date);
const increment = event.deltaY > 0 ? -1 : 1;
newDate.setDate(newDate.getDate() + increment);
if (newDate >= (date.from as Date)) {
onDateSelect({ from: new Date(date.from as Date), to: newDate });
setMonthTo(newDate);
}
} else if (highlightedPart === "secondMonth") {
const currentMonth = monthTo ? monthTo.getMonth() : 0;
const newMonthIndex = currentMonth + (event.deltaY > 0 ? -1 : 1);
handleMonthChange(newMonthIndex, "to");
} else if (highlightedPart === "secondYear" && yearTo !== undefined) {
const newYear = yearTo + (event.deltaY > 0 ? -1 : 1);
handleYearChange(newYear, "to");
}
};

React.useEffect(() => {
const firstDayElement = document.getElementById(`firstDay-${id}`);
const firstMonthElement = document.getElementById(`firstMonth-${id}`);
const firstYearElement = document.getElementById(`firstYear-${id}`);
const secondDayElement = document.getElementById(`secondDay-${id}`);
const secondMonthElement = document.getElementById(`secondMonth-${id}`);
const secondYearElement = document.getElementById(`secondYear-${id}`);

const elements = [
firstDayElement,
firstMonthElement,
firstYearElement,
secondDayElement,
secondMonthElement,
secondYearElement,
];

const addPassiveEventListener = (element: HTMLElement | null) => {
if (element) {
element.addEventListener(
"wheel",
handleWheel as unknown as EventListener,
{
passive: false,
}
);
}
};

elements.forEach(addPassiveEventListener);

return () => {
elements.forEach((element) => {
if (element) {
element.removeEventListener(
"wheel",
handleWheel as unknown as EventListener
);
}
});
};
}, [highlightedPart, date]);

const formatWithTz = (date: Date, fmt: string) =>
formatInTimeZone(date, timeZone, fmt);

return (
<>

{`
.date-part {
touch-action: none;
}
`}






{date?.from ? (
date.to ? (
<>
handleMouseOver("firstDay")}
onMouseLeave={handleMouseLeave}
>
{formatWithTz(date.from, "dd")}
{" "}
handleMouseOver("firstMonth")}
onMouseLeave={handleMouseLeave}
>
{formatWithTz(date.from, "LLL")}

,{" "}
handleMouseOver("firstYear")}
onMouseLeave={handleMouseLeave}
>
{formatWithTz(date.from, "y")}

{numberOfMonths === 2 && (
<>
{" - "}
handleMouseOver("secondDay")}
onMouseLeave={handleMouseLeave}
>
{formatWithTz(date.to, "dd")}
{" "}
handleMouseOver("secondMonth")}
onMouseLeave={handleMouseLeave}
>
{formatWithTz(date.to, "LLL")}

,{" "}
handleMouseOver("secondYear")}
onMouseLeave={handleMouseLeave}
>
{formatWithTz(date.to, "y")}

>
)}
>
) : (
<>
handleMouseOver("day")}
onMouseLeave={handleMouseLeave}
>
{formatWithTz(date.from, "dd")}
{" "}
handleMouseOver("month")}
onMouseLeave={handleMouseLeave}
>
{formatWithTz(date.from, "LLL")}

,{" "}
handleMouseOver("year")}
onMouseLeave={handleMouseLeave}
>
{formatWithTz(date.from, "y")}

>
)
) : (
Pick a date
)}



{isPopoverOpen && (


{numberOfMonths === 2 && (

{dateRanges.map(({ label, start, end }) => (
{
selectDateRange(start, end, label);
setMonthFrom(start);
setYearFrom(start.getFullYear());
setMonthTo(end);
setYearTo(end.getFullYear());
}}
>
{label}

))}

)}



{
handleMonthChange(months.indexOf(value), "from");
setSelectedRange(null);
}}
value={
monthFrom ? months[monthFrom.getMonth()] : undefined
}
>




{months.map((month, idx) => (

{month}

))}


{
handleYearChange(Number(value), "from");
setSelectedRange(null);
}}
value={yearFrom ? yearFrom.toString() : undefined}
>




{years.map((year, idx) => (

{year}

))}



{numberOfMonths === 2 && (

{
handleMonthChange(months.indexOf(value), "to");
setSelectedRange(null);
}}
value={
monthTo ? months[monthTo.getMonth()] : undefined
}
>




{months.map((month, idx) => (

{month}

))}


{
handleYearChange(Number(value), "to");
setSelectedRange(null);
}}
value={yearTo ? yearTo.toString() : undefined}
>




{years.map((year, idx) => (

{year}

))}



)}







)}

>
);
}
);

CalendarDatePicker.displayName = "CalendarDatePicker";
```

### Step 3: Integrate the Component

Update `page.tsx`:

```tsx
// src/app/page.tsx

"use client";

import React, { useState } from "react";
import { CalendarDatePicker } from "@/components/calendar-date-picker";

function Home() {
const [selectedDateRange, setSelectedDateRange] = useState({
from: new Date(new Date().getFullYear(), 0, 1),
to: new Date(),
});

return (



Calendar Date Picker Component




Selected Date Range:



{selectedDateRange.from.toDateString()} -{" "}
{selectedDateRange.to.toDateString()}




);
}

export default Home;
```

### Step 4: Run Your Project

```bash
npm run dev
```