From e81dfc19a4ad39a1884ad93c13634ecc57d9ac21 Mon Sep 17 00:00:00 2001 From: "Lukas.J.Han" Date: Sun, 15 Sep 2024 17:41:03 +0900 Subject: [PATCH] feat: add calendar component Signed-off-by: Lukas.J.Han --- packages/core/lib/components/Calendar.tsx | 193 ++++++++++++++++++++++ packages/core/lib/index.ts | 2 + stories/core/Calendar.stories.ts | 39 +++++ 3 files changed, 234 insertions(+) create mode 100644 packages/core/lib/components/Calendar.tsx create mode 100644 stories/core/Calendar.stories.ts diff --git a/packages/core/lib/components/Calendar.tsx b/packages/core/lib/components/Calendar.tsx new file mode 100644 index 0000000..3bc3888 --- /dev/null +++ b/packages/core/lib/components/Calendar.tsx @@ -0,0 +1,193 @@ +import React, { useState } from 'react'; +import { Label } from './Label'; + +interface CalendarProps { + mode: 'single' | 'range'; + onSelect: (dates: string[]) => void; +} + +const today = new Date(); +const DAYS = ['일', '월', '화', '수', '목', '금', '토']; +const MONTHS = [ + '1월', + '2월', + '3월', + '4월', + '5월', + '6월', + '7월', + '8월', + '9월', + '10월', + '11월', + '12월', +]; + +const TriangleIcon: React.FC<{ direction: 'left' | 'right' }> = ({ + direction, +}) => ( + + + +); + +export const Calendar: React.FC = ({ mode, onSelect }) => { + const [currentDate, setCurrentDate] = useState(new Date()); + const [selectedDates, setSelectedDates] = useState([]); + + const getDaysInMonth = (date: Date): Date[] => { + const year = date.getFullYear(); + const month = date.getMonth(); + const days = new Date(year, month + 1, 0).getDate(); + return Array.from({ length: days }, (_, i) => new Date(year, month, i + 1)); + }; + + const handleDateClick = (date: Date) => { + if (mode === 'single') { + setSelectedDates([date]); + onSelect([date.toISOString().split('T')[0]]); + } else { + if (selectedDates.length === 0 || selectedDates.length === 2) { + setSelectedDates([date]); + } else { + const [start] = selectedDates; + const end = date; + setSelectedDates(start <= end ? [start, end] : [end, start]); + onSelect([start, end].map((d) => d.toISOString().split('T')[0])); + } + } + }; + + const isDateInRange = (date: Date): boolean => { + if (selectedDates.length !== 2) return false; + const [start, end] = selectedDates; + return date >= start && date <= end; + }; + + const isToday = (date: Date): boolean => { + const today = new Date(); + return ( + date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear() + ); + }; + + const changeMonth = (delta: number) => { + setCurrentDate( + new Date(currentDate.getFullYear(), currentDate.getMonth() + delta, 1) + ); + }; + + const renderCalendar = () => { + const days = getDaysInMonth(currentDate); + const firstDayOfMonth = days[0].getDay(); + + return ( +
+ {DAYS.map((day) => ( +
+ +
+ ))} + {Array.from({ length: firstDayOfMonth }).map((_, index) => ( +
+ ))} + {days.map((date) => ( +
handleDateClick(date)} + className={` + cursor-pointer text-center px-2 py-1 rounded-4 + ${isDateInRange(date) || selectedDates.some((d) => d.getTime() === date.getTime()) ? 'bg-primary text-gray-0' : ''} + ${isToday(date) ? 'border border-primary' : 'border border-transparent'} + `} + > + +
+ ))} +
+ ); + }; + + return ( +
+
+ +
+ + +
+ +
+ {renderCalendar()} +
+ ); +}; diff --git a/packages/core/lib/index.ts b/packages/core/lib/index.ts index 5a41ad4..33ade0b 100644 --- a/packages/core/lib/index.ts +++ b/packages/core/lib/index.ts @@ -25,6 +25,7 @@ import { Modal } from './components/Modal'; import { Accordion } from './components/Accordion'; import { Disclosure } from './components/Disclosure'; import { Pagination } from './components/Pagination'; +import { Calendar } from './components/Calendar'; export { Display, Heading, Title, Body, Detail, Label, Link, colors }; export { @@ -45,4 +46,5 @@ export { Accordion, Disclosure, Pagination, + Calendar, }; diff --git a/stories/core/Calendar.stories.ts b/stories/core/Calendar.stories.ts new file mode 100644 index 0000000..4e9a666 --- /dev/null +++ b/stories/core/Calendar.stories.ts @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Calendar } from '../../packages/core/lib'; + +const meta = { + title: 'Components/Calendar', + component: Calendar, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + mode: { + control: { + type: 'select', + options: ['single', 'range'], + }, + }, + onSelect: { action: 'clicked' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + mode: 'single', + onSelect: (date: string[]) => console.log(`Selected date: ${date[0]}`), + }, +}; + +export const Range: Story = { + args: { + mode: 'range', + onSelect: (date: string[]) => + console.log(`Selected date: ${date[0]} ~ ${date[1]}`), + }, +};