Calendar
Um calendário para seleção de datas
Código:
'use client';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
dayjs.extend(isSameOrBefore);
dayjs.extend(isBetween);
import { cn } from '@/lib/utils';
import * as React from 'react';
import { useStore } from 'zustand';
import { textVariants } from '../text';
import { createCalendarStore } from './useCalendar';
export interface DateIndicator {
date: Date;
color?: string;
}
export interface CalendarProps {
startDate?: Date | null;
endDate?: Date | null;
range?: boolean;
onRangeChange?: (start: Date | null, end: Date | null) => void;
availability?: Record<string, boolean> | string[];
indicators?: DateIndicator[];
className?: string;
monthsBefore?: number;
monthsAfter?: number;
disablePastDates?: boolean;
disabledDates?: Date[];
disabledWeekdays?: number[];
allowClickOnDisabled?: boolean;
}
const WEEKDAYS = ['D', 'S', 'T', 'Q', 'Q', 'S', 'S'];
function Calendar({
range = false,
onRangeChange,
indicators = [],
className,
monthsBefore = 12,
monthsAfter = 12,
disablePastDates = true,
disabledDates = [],
disabledWeekdays = [],
allowClickOnDisabled = false,
}: CalendarProps) {
const [store] = React.useState(() => createCalendarStore());
const { startDate, endDate, selectDate, setRange } = useStore(store);
const today = dayjs();
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
const currentMonthRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
setRange(range);
}, [range, setRange]);
React.useEffect(() => {
onRangeChange?.(startDate, endDate);
}, [startDate, endDate, onRangeChange]);
const months = React.useMemo(() => {
const result: dayjs.Dayjs[] = [];
for (let i = -monthsBefore; i <= monthsAfter; i++) {
result.push(today.add(i, 'month'));
}
return result;
}, [monthsBefore, monthsAfter]);
React.useEffect(() => {
if (currentMonthRef.current && scrollContainerRef.current) {
const container = scrollContainerRef.current;
const currentMonth = currentMonthRef.current;
const containerTop = container.getBoundingClientRect().top;
const monthTop = currentMonth.getBoundingClientRect().top;
const relativePosition = monthTop - containerTop;
container.scrollTo({
top: container.scrollTop + relativePosition,
behavior: 'smooth',
});
}
}, []);
const isDayDisabled = (day: Date) => {
if (disablePastDates && dayjs(day).isBefore(dayjs(), 'day')) {
return true;
}
if (disabledWeekdays.length > 0 && disabledWeekdays.includes(dayjs(day).day())) {
return true;
}
return disabledDates.some((d) => dayjs(day).isSame(dayjs(d), 'day'));
};
const isStartDay = (day: Date) => !!(startDate && dayjs(day).isSame(dayjs(startDate), 'day'));
const isEndDay = (day: Date) => !!(endDate && dayjs(day).isSame(dayjs(endDate), 'day'));
const isInRange = (day: Date) => {
if (!range || !startDate || !endDate) return false;
return dayjs(day).isBetween(dayjs(startDate), dayjs(endDate), 'day', '[]');
};
const getIndicator = (date: Date) => {
const key = dayjs(date).format('YYYY-MM-DD');
return indicators.find((ind) => dayjs(ind.date).format('YYYY-MM-DD') === key);
};
return (
<div className={cn('flex h-80 max-w-[500px] flex-col overflow-hidden', className)}>
<div className='sticky top-0 z-10 pb-2'>
<div className='grid grid-cols-7 gap-1 px-2'>
{WEEKDAYS.map((day, index) => (
<div
key={index}
className={cn('text-emphasis-low flex items-center justify-center', textVariants({ typography: 'description-1' }))}
>
{day}
</div>
))}
</div>
</div>
<div ref={scrollContainerRef} className='scrollbar-none flex-1 overflow-y-auto px-2'>
{months.map((month, monthIndex) => {
const isCurrentMonth = month.isSame(today, 'month');
return (
<MonthView
key={monthIndex}
ref={isCurrentMonth ? currentMonthRef : null}
month={month.toDate()}
isStartDay={isStartDay}
isEndDay={isEndDay}
isInRange={isInRange}
getIndicator={getIndicator}
allowClickOnDisabled={allowClickOnDisabled}
onDayClick={(day) => {
if (!isDayDisabled(day) || allowClickOnDisabled) {
selectDate(day);
}
}}
isDayDisabled={isDayDisabled}
/>
);
})}
</div>
</div>
);
}
export { Calendar };
yarn add dayjs zustand
Prévia:
Preview interativo (intervalo)
D
S
T
Q
Q
S
S
June de 2026
July de 2026
August de 2026
September de 2026
October de 2026
November de 2026
December de 2026
January de 2027
February de 2027
March de 2027
April de 2027
May de 2027
June de 2027
July de 2027
August de 2027
Range
Disable past dates
Allow click on disabled
Disable weekends
Months before
2
Months after
12
Props:
| Property | Type | Default | Description |
|---|---|---|---|
range | boolean | false | Habilita a seleção de um intervalo de datas (início e fim). |
onRangeChange | (start: Date | null, end: Date | null) => void | - | Callback disparado quando as datas selecionadas mudam. |
availability | Record<string, boolean> | string[] | - | Define a disponibilidade de cada dia. |
indicators | DateIndicator[] | [] | Marcadores coloridos exibidos em datas específicas. |
monthsBefore | number | 12 | Quantidade de meses exibidos antes do mês atual. |
monthsAfter | number | 12 | Quantidade de meses exibidos após o mês atual. |
disablePastDates | boolean | true | Bloqueia a seleção de datas anteriores a hoje. |
disabledDates | Date[] | [] | Lista de datas específicas desabilitadas para seleção. |
disabledWeekdays | number[] | [] | Dias da semana desabilitados (0 = domingo … 6 = sábado). |
allowClickOnDisabled | boolean | false | Permite clicar em dias desabilitados mesmo assim. |
