// Adapted from Kibo UI (MIT), with owned React/CSS APIs. See THIRD_PARTY_NOTICES.md. "use client"; import { useState, type ComponentProps, type ReactNode } from "react"; export interface CalendarEvent { id: string; date: Date; title: string; } export type CalendarProps = Omit, "onChange"> & { month?: Date; defaultMonth?: Date; onMonthChange?: (month: Date) => void; events: readonly CalendarEvent[]; onEventClick?: (event: CalendarEvent) => void; renderEvent?: (event: CalendarEvent) => ReactNode; locale?: string; weekStartsOn?: 0 | 1; }; export function Calendar({ month, defaultMonth = new Date(), onMonthChange, events, onEventClick, renderEvent, locale = "en-US", weekStartsOn = 0, className = "", ...props }: CalendarProps) { const [internal, setInternal] = useState(defaultMonth); const current = month ?? internal; const year = current.getFullYear(), m = current.getMonth(); const offset = (new Date(year, m, 1).getDay() - weekStartsOn + 7) % 7; const length = new Date(year, m + 1, 0).getDate(); const cells = Math.ceil((offset + length) / 7) * 7; const navigate = (step: number) => { const next = new Date(year, m + step, 1); if (month === undefined) setInternal(next); onMonthChange?.(next); }; return (
{current.toLocaleDateString(locale, { month: "long", year: "numeric", })}
{Array.from({ length: 7 }, (_, i) => ( ))} {Array.from({ length: cells / 7 }, (_, week) => ( {Array.from({ length: 7 }, (_, day) => { const date = new Date(year, m, week * 7 + day - offset + 1); const matches = events.filter( (event) => event.date.toDateString() === date.toDateString(), ); return ( ); })} ))}
Event calendar
{new Date(2023, 0, 1 + weekStartsOn + i).toLocaleDateString( locale, { weekday: "short" }, )}
{matches.map((event) => onEventClick ? ( ) : (
{renderEvent?.(event) ?? event.title}
), )}
); }