feat: button, calendar, datePicker, input, pagination, icons 추가
This commit is contained in:
182
web-app/app/shared/components/calendar/Calendar.tsx
Normal file
182
web-app/app/shared/components/calendar/Calendar.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
Button as AriaButton,
|
||||
Calendar as AriaCalendar,
|
||||
CalendarGridHeader as AriaCalendarGridHeader,
|
||||
CalendarCell,
|
||||
CalendarGrid,
|
||||
CalendarGridBody,
|
||||
CalendarHeaderCell,
|
||||
Heading,
|
||||
} from 'react-aria-components';
|
||||
|
||||
import { getLocalTimeZone, today } from '@internationalized/date';
|
||||
import { tv } from 'tailwind-variants';
|
||||
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@/components/icons';
|
||||
|
||||
import { calendarDateToDate, dateToCalendarDate } from './utils';
|
||||
|
||||
const cellStyles = tv({
|
||||
base: 'flex h-[30px] w-[30px] cursor-default items-center justify-center rounded-full text-xs outline-none',
|
||||
variants: {
|
||||
isSelected: {
|
||||
true: 'bg-primary font-bold text-white',
|
||||
false: '',
|
||||
},
|
||||
isDisabled: {
|
||||
true: 'cursor-default text-kc-gray-be',
|
||||
},
|
||||
isUnavailable: {
|
||||
true: 'cursor-default text-kc-gray-be line-through',
|
||||
},
|
||||
isOutsideMonth: {
|
||||
true: 'text-kc-gray-be',
|
||||
},
|
||||
isSunday: {
|
||||
true: 'text-[#e48686]',
|
||||
},
|
||||
isSaturday: {
|
||||
true: 'text-[#7b8cc8]',
|
||||
},
|
||||
isToday: {
|
||||
true: 'font-bold text-primary',
|
||||
},
|
||||
isHovered: {
|
||||
true: 'bg-primary-tertiary01',
|
||||
},
|
||||
isFocusVisible: {
|
||||
true: 'ring-2 ring-offset-2 ring-primary',
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
isSelected: true,
|
||||
isSunday: true,
|
||||
className: 'text-white',
|
||||
},
|
||||
{
|
||||
isSelected: true,
|
||||
isSaturday: true,
|
||||
className: 'text-white',
|
||||
},
|
||||
{
|
||||
isSelected: true,
|
||||
isToday: true,
|
||||
className: 'text-white',
|
||||
},
|
||||
{
|
||||
isSelected: true,
|
||||
isHovered: true,
|
||||
className: 'bg-primary',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export interface CalendarProps {
|
||||
value?: Date | null;
|
||||
defaultValue?: Date | null;
|
||||
onChange?: (date: Date | null) => void;
|
||||
maxValue?: Date | null;
|
||||
minValue?: Date | null;
|
||||
isDisabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Calendar({ value, defaultValue, onChange, maxValue, minValue, isDisabled, className }: CalendarProps) {
|
||||
const todayDate = today(getLocalTimeZone());
|
||||
|
||||
return (
|
||||
<AriaCalendar
|
||||
aria-label="날짜 선택"
|
||||
value={value !== undefined ? dateToCalendarDate(value ?? null) : undefined}
|
||||
defaultValue={defaultValue !== undefined ? dateToCalendarDate(defaultValue ?? null) : undefined}
|
||||
onChange={(val) => onChange?.(calendarDateToDate(val))}
|
||||
maxValue={maxValue !== undefined ? dateToCalendarDate(maxValue) : undefined}
|
||||
minValue={minValue !== undefined ? dateToCalendarDate(minValue) : undefined}
|
||||
isDisabled={isDisabled}
|
||||
firstDayOfWeek="sun"
|
||||
className={className}
|
||||
>
|
||||
<CalendarHeader />
|
||||
<CalendarGrid className="border-spacing-0">
|
||||
<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,
|
||||
isDisabled: isCellDisabled,
|
||||
isUnavailable,
|
||||
isOutsideMonth,
|
||||
isHovered,
|
||||
isFocusVisible,
|
||||
}) =>
|
||||
cellStyles({
|
||||
isSelected,
|
||||
isDisabled: isCellDisabled,
|
||||
isUnavailable,
|
||||
isOutsideMonth,
|
||||
isSunday: !isSelected && !isDisabled && !isOutsideMonth ? isSunday : false,
|
||||
isSaturday: !isSelected && !isDisabled && !isOutsideMonth ? isSaturday : false,
|
||||
isToday: !isSelected ? isToday : false,
|
||||
isHovered: !isSelected ? isHovered : false,
|
||||
isFocusVisible,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</CalendarGridBody>
|
||||
</CalendarGrid>
|
||||
</AriaCalendar>
|
||||
);
|
||||
}
|
||||
|
||||
const navButton = tv({
|
||||
base: 'flex h-7 w-7 items-center justify-center rounded-sm border-none bg-transparent outline-none',
|
||||
variants: {
|
||||
isDisabled: {
|
||||
true: 'cursor-default text-kc-gray-be',
|
||||
false: 'cursor-pointer text-kc-black-34 hover:bg-kc-gray-eb',
|
||||
},
|
||||
isFocusVisible: {
|
||||
true: 'ring-2 ring-offset-2 ring-primary',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
isDisabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
export function CalendarHeader() {
|
||||
return (
|
||||
<header className="flex items-center gap-1 pb-3">
|
||||
<AriaButton slot="previous" className={(renderProps) => navButton(renderProps)}>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</AriaButton>
|
||||
<Heading className="mx-2 flex-1 text-center text-sm font-semibold text-kc-black-34" />
|
||||
<AriaButton slot="next" className={(renderProps) => navButton(renderProps)}>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</AriaButton>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function CalendarGridHeader() {
|
||||
return (
|
||||
<AriaCalendarGridHeader>
|
||||
{(day) => (
|
||||
<CalendarHeaderCell className="h-[30px] w-[30px] text-xs font-semibold text-kc-gray-99 first:text-[#e48686] last:text-[#7b8cc8]">
|
||||
{day}
|
||||
</CalendarHeaderCell>
|
||||
)}
|
||||
</AriaCalendarGridHeader>
|
||||
);
|
||||
}
|
||||
195
web-app/app/shared/components/calendar/RangeCalendar.tsx
Normal file
195
web-app/app/shared/components/calendar/RangeCalendar.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
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-kc-gray-be',
|
||||
},
|
||||
isOutsideMonth: {
|
||||
true: 'text-kc-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>
|
||||
);
|
||||
}
|
||||
4
web-app/app/shared/components/calendar/index.ts
Normal file
4
web-app/app/shared/components/calendar/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { Calendar } from './Calendar';
|
||||
export type { CalendarProps } from './Calendar';
|
||||
export { RangeCalendar } from './RangeCalendar';
|
||||
export type { RangeCalendarProps } from './RangeCalendar';
|
||||
13
web-app/app/shared/components/calendar/utils.ts
Normal file
13
web-app/app/shared/components/calendar/utils.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { CalendarDate, type DateValue, fromDate, toCalendarDate } from '@internationalized/date';
|
||||
|
||||
const TIMEZONE = 'Asia/Seoul';
|
||||
|
||||
export function dateToCalendarDate(date: Date | null): CalendarDate | null {
|
||||
if (!date) return null;
|
||||
return toCalendarDate(fromDate(date, TIMEZONE));
|
||||
}
|
||||
|
||||
export function calendarDateToDate(value: DateValue | null): Date | null {
|
||||
if (!value) return null;
|
||||
return value.toDate(TIMEZONE);
|
||||
}
|
||||
Reference in New Issue
Block a user