Skip to main content
Glama
calendar.tsx14 kB
import { Button, buttonVariants } from '@/components/ui/button' import { cn } from '@/lib/utils' import { differenceInCalendarDays } from 'date-fns' import { ChevronLeft, ChevronRight } from 'lucide-react' import * as React from 'react' import { DayPicker, labelNext, labelPrevious, useDayPicker, type DayPickerProps, } from 'react-day-picker' import { useLanguage } from '@/contexts/language-context' import { enUS, zhCN } from 'date-fns/locale' export type CalendarProps = DayPickerProps & { /** * In the year view, the number of years to display at once. * @default 12 */ yearRange?: number /** * Wether to show the year switcher in the caption. * @default true */ showYearSwitcher?: boolean 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 } 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({ className, showOutsideDays = true, showYearSwitcher = true, yearRange = 12, numberOfMonths, components, ...props }: CalendarProps) { const [navView, setNavView] = React.useState<NavView>('days') const [displayYears, setDisplayYears] = React.useState<{ from: number to: number }>( React.useMemo(() => { const currentYear = new Date().getFullYear() return { from: currentYear - Math.floor(yearRange / 2 - 1), to: currentYear + Math.ceil(yearRange / 2), } }, [yearRange]) ) const { onNextClick, onPrevClick, startMonth, endMonth } = props const columnsDisplayed = navView === 'years' ? 1 : numberOfMonths 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) const buttonNavClassName = buttonVariants({ variant: 'outline', className: 'absolute h-7 w-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, 'day-range-start rounded-s-md', props.rangeStartClassName ) const _rangeEndClassName = cn( buttonRangeClassName, 'day-range-end 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( 'day-outside 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 { language } = useLanguage() return ( <DayPicker showOutsideDays={showOutsideDays} className={cn('p-3', className)} locale={language === 'zh-CN' ? zhCN : enUS} // 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, week_number_header: 'size-8 mr-2', week_number: 'size-8 mr-2 text-sm leading-8 font-thin text-neutral-400 border-r', }} components={{ Chevron: ({ orientation }) => { const Icon = orientation === 'left' ? ChevronLeft : ChevronRight return <Icon className='h-4 w-4' /> }, Nav: ({ className }) => ( <Nav className={className} displayYears={displayYears} navView={navView} setDisplayYears={setDisplayYears} startMonth={startMonth} endMonth={endMonth} onPrevClick={onPrevClick} onNextClick={onNextClick} /> ), CaptionLabel: (props) => ( <CaptionLabel showYearSwitcher={showYearSwitcher} navView={navView} setNavView={setNavView} displayYears={displayYears} {...props} /> ), MonthGrid: ({ className, children, ...props }) => ( <MonthGrid children={children} className={className} displayYears={displayYears} startMonth={startMonth} endMonth={endMonth} navView={navView} setNavView={setNavView} {...props} /> ), ...components, }} numberOfMonths={columnsDisplayed} {...props} /> ) } Calendar.displayName = 'Calendar' function Nav({ className, navView, startMonth, endMonth, displayYears, setDisplayYears, onPrevClick, onNextClick, }: { className?: string navView: NavView startMonth?: Date endMonth?: Date displayYears: { from: number; to: number } setDisplayYears: React.Dispatch<React.SetStateAction<{ from: number; to: number }>> onPrevClick?: (date: Date) => void onNextClick?: (date: Date) => void }) { const { nextMonth, previousMonth, goToMonth } = useDayPicker() 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 })() 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 })() const handlePreviousClick = React.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]) const handleNextClick = React.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 h-7 w-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='h-4 w-4' /> </Button> <Button variant='outline' className='absolute right-0 h-7 w-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='h-4 w-4' /> </Button> </nav> ) } function CaptionLabel({ children, showYearSwitcher, navView, setNavView, displayYears, ...props }: { showYearSwitcher?: boolean navView: NavView setNavView: React.Dispatch<React.SetStateAction<NavView>> displayYears: { from: number; to: number } } & React.HTMLAttributes<HTMLSpanElement>) { 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> ) } function MonthGrid({ className, children, displayYears, startMonth, endMonth, navView, setNavView, ...props }: { className?: string children: React.ReactNode displayYears: { from: number; to: number } startMonth?: Date endMonth?: Date navView: NavView setNavView: React.Dispatch<React.SetStateAction<NavView>> } & React.TableHTMLAttributes<HTMLTableElement>) { 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> ) } function YearGrid({ className, displayYears, startMonth, endMonth, setNavView, navView, ...props }: { className?: string displayYears: { from: number; to: number } startMonth?: Date endMonth?: Date setNavView: React.Dispatch<React.SetStateAction<NavView>> navView: NavView } & React.HTMLAttributes<HTMLDivElement>) { const { goToMonth, selected } = useDayPicker() 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), startMonth!) < 0 const isAfter = differenceInCalendarDays(new Date(displayYears.from + i, 0, 0), endMonth!) > 0 const isDisabled = isBefore || isAfter return ( <Button key={i} className={cn( 'h-7 w-full text-sm font-normal text-foreground', displayYears.from + i === new Date().getFullYear() && 'bg-accent font-medium text-accent-foreground' )} 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> ) } export { Calendar }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/itcook/graphiti-mcp-pro'

If you have feedback or need assistance with the MCP directory API, please join our Discord server