196 lines
5.5 KiB
TypeScript
196 lines
5.5 KiB
TypeScript
import {
|
|
RangeCalendar as AriaRangeCalendar,
|
|
CalendarCell,
|
|
CalendarGrid,
|
|
CalendarGridBody,
|
|
type DateValue,
|
|
} from 'react-aria-components';
|
|
|
|
import { getLocalTimeZone, today } from '@internationalized/date';
|
|
import { tv } from 'tailwind-variants';
|
|
|
|
import { CalendarGridHeader, CalendarHeader } from './Calendar';
|
|
import { calendarDateToDate, dateToCalendarDate } from './utils';
|
|
|
|
const rangeCell = tv({
|
|
base: 'flex h-full w-full items-center justify-center rounded-full text-xs',
|
|
variants: {
|
|
selectionState: {
|
|
none: '',
|
|
middle: '',
|
|
cap: 'bg-primary font-bold text-white',
|
|
},
|
|
isDisabled: {
|
|
true: 'text-dabeeo-gray-be',
|
|
},
|
|
isOutsideMonth: {
|
|
true: 'text-dabeeo-gray-be',
|
|
},
|
|
isSunday: {
|
|
true: 'text-[#e48686]',
|
|
},
|
|
isSaturday: {
|
|
true: 'text-[#7b8cc8]',
|
|
},
|
|
isToday: {
|
|
true: 'font-bold text-primary',
|
|
},
|
|
isHovered: {
|
|
true: '',
|
|
},
|
|
isFocusVisible: {
|
|
true: 'ring-2 ring-offset-2 ring-primary',
|
|
},
|
|
},
|
|
compoundVariants: [
|
|
{
|
|
selectionState: 'cap',
|
|
isSunday: true,
|
|
className: 'text-white',
|
|
},
|
|
{
|
|
selectionState: 'cap',
|
|
isSaturday: true,
|
|
className: 'text-white',
|
|
},
|
|
{
|
|
selectionState: 'cap',
|
|
isToday: true,
|
|
className: 'text-white',
|
|
},
|
|
{
|
|
selectionState: 'none',
|
|
isHovered: true,
|
|
className: 'bg-primary-tertiary01 rounded-full',
|
|
},
|
|
{
|
|
selectionState: 'middle',
|
|
isHovered: true,
|
|
className: 'bg-primary-tertiary01',
|
|
},
|
|
],
|
|
});
|
|
|
|
export interface RangeCalendarProps {
|
|
value?: { start: Date; end: Date } | null;
|
|
defaultValue?: { start: Date; end: Date } | null;
|
|
onChange?: (value: { start: Date; end: Date } | null) => void;
|
|
maxValue?: Date | null;
|
|
minValue?: Date | null;
|
|
isDisabled?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export function RangeCalendar({
|
|
value,
|
|
defaultValue,
|
|
onChange,
|
|
maxValue,
|
|
minValue,
|
|
isDisabled,
|
|
className,
|
|
}: RangeCalendarProps) {
|
|
const todayDate = today(getLocalTimeZone());
|
|
|
|
const ariaValue =
|
|
value !== undefined
|
|
? value
|
|
? { start: dateToCalendarDate(value.start)!, end: dateToCalendarDate(value.end)! }
|
|
: null
|
|
: undefined;
|
|
|
|
const ariaDefaultValue =
|
|
defaultValue !== undefined
|
|
? defaultValue
|
|
? { start: dateToCalendarDate(defaultValue.start)!, end: dateToCalendarDate(defaultValue.end)! }
|
|
: null
|
|
: undefined;
|
|
|
|
return (
|
|
<AriaRangeCalendar<DateValue>
|
|
aria-label="날짜 범위 선택"
|
|
value={ariaValue}
|
|
defaultValue={ariaDefaultValue}
|
|
onChange={(range) => {
|
|
if (!range) {
|
|
onChange?.(null);
|
|
} else {
|
|
onChange?.({
|
|
start: calendarDateToDate(range.start)!,
|
|
end: calendarDateToDate(range.end)!,
|
|
});
|
|
}
|
|
}}
|
|
maxValue={maxValue !== undefined ? dateToCalendarDate(maxValue) : undefined}
|
|
minValue={minValue !== undefined ? dateToCalendarDate(minValue) : undefined}
|
|
isDisabled={isDisabled}
|
|
firstDayOfWeek="sun"
|
|
className={className}
|
|
>
|
|
<CalendarHeader />
|
|
<CalendarGrid className="border-spacing-0 [&_td]:px-0 [&_td]:py-px">
|
|
<CalendarGridHeader />
|
|
<CalendarGridBody>
|
|
{(date) => {
|
|
const dayOfWeek = date.toDate('UTC').getDay();
|
|
const isSunday = dayOfWeek === 0;
|
|
const isSaturday = dayOfWeek === 6;
|
|
const isToday = date.compare(todayDate) === 0;
|
|
|
|
return (
|
|
<CalendarCell
|
|
date={date}
|
|
className={({ isSelected, isSelectionStart, isSelectionEnd }) => {
|
|
const classes = ['group h-[30px] w-[30px] cursor-default text-sm outline-none'];
|
|
if (isSelected) {
|
|
classes.push('bg-primary/10');
|
|
if (isSelectionStart) classes.push('rounded-s-full');
|
|
if (isSelectionEnd) classes.push('rounded-e-full');
|
|
}
|
|
return classes.join(' ');
|
|
}}
|
|
>
|
|
{({
|
|
formattedDate,
|
|
isSelected,
|
|
isSelectionStart,
|
|
isSelectionEnd,
|
|
isDisabled: isCellDisabled,
|
|
isOutsideMonth,
|
|
isHovered,
|
|
isFocusVisible,
|
|
}) => {
|
|
const selectionState =
|
|
isSelected && (isSelectionStart || isSelectionEnd)
|
|
? ('cap' as const)
|
|
: isSelected
|
|
? ('middle' as const)
|
|
: ('none' as const);
|
|
const showDayColor = selectionState !== 'cap' && !isCellDisabled && !isOutsideMonth;
|
|
|
|
return (
|
|
<span
|
|
className={rangeCell({
|
|
selectionState,
|
|
isDisabled: isCellDisabled,
|
|
isOutsideMonth,
|
|
isSunday: showDayColor ? isSunday : false,
|
|
isSaturday: showDayColor ? isSaturday : false,
|
|
isToday: selectionState !== 'cap' ? isToday : false,
|
|
isHovered: !isSelected ? isHovered : false,
|
|
isFocusVisible,
|
|
})}
|
|
>
|
|
{formattedDate}
|
|
</span>
|
|
);
|
|
}}
|
|
</CalendarCell>
|
|
);
|
|
}}
|
|
</CalendarGridBody>
|
|
</CalendarGrid>
|
|
</AriaRangeCalendar>
|
|
);
|
|
}
|