-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: 한국 공휴일을 계산해 주는 라이브러리 추가 * chore: 이전 달력 컴포넌트 & 로직 레거시 폴더로 이동 * chore: 이전에 사용했던 달력 데이터 계산 로직 커스텀 훅 삭제 * feat: 재사용할 수 있는 달력 공통 컴포넌트 구현 - useCalendar 커스텀 훅이 반환하는 달력 데이터를 각 컴포넌트가 뽑아서 사용할 수 있도록 Context API 활용 - Header, Weekdays, Body 컴포넌트로 달력을 구성하는 컴포넌트들을 합성 컴포넌트 구조로 구현 * feat: 공통 달력 컴포넌트, 헤더 컴포넌트 구현 - render props 패턴을 활용하여 useCalendar 컴포넌트가 반환하는 월, 일 데이터를 사용해서 UI를 그리는 컴포넌트를 호출할 수 있도록 구현 * feat: 공통 달력 컴포넌트, 일주일 날짜를 표현하는 컴포넌트 구현 - render props 패턴을 활용하여 useCalendar 컴포넌트가 반환하는 일주일 날짜 데이터를 사용해서 UI를 그리는 컴포넌트를 호출할 수 있도록 구현 * feat: 공통 달력 컴포넌트, 한 달을 구성하는 모든 날짜를 표현하는 컴포넌트 구현 - render props 패턴을 활용하여 useCalendar 컴포넌트가 반환하는 날짜 데이터를 사용해서 UI를 그리는 컴포넌트를 호출할 수 있도록 구현 * feat: 약속을 생성할 때 필요한 달력 컴포넌트 - 헤더 컴포넌트 구현 * feat: 약속을 생성할 때 필요한 달력 컴포넌트 - 일주일 날짜 컴포넌트 구현 * feat: 하나씩, 기간으로 날짜를 선택할 수 있는 인터페이스를 제공하는 useDateSelect 커스텀 훅 구현 - 날짜를 선택할 수 있는 모드인 single, range를 상태로 관리 - 시작/끝 날짜가 모두 선택되면 그 사이 모든 날짜가 선택되도록 getDatesInRange 유틸 함수 구현 - 끝 날짜가 시작 날짜보다 이른 경우 해당 끝 날짜를 다시 시작 날짜로 변경하도록 예외 처리 - 모든 범위 날짜가 선택되었을 때, 다른 날짜를 선택한 경우 해당 다른 날짜를 시작 날짜로 변경하도록 예외 처리 * feat: 한 달의 날짜 데이터, 다음/이전 달로 이동시킬 수 있는 인터페이스를 제공하는 useCalendar 커스텀 훅 구현 * chore: 폴더명 변경에 따른 import 경로 수정 * chore: 공통 달력 컴포넌트, useCalendar 커스텀 훅에서 필요한 타입 정의 * feat: 달, 년(year), 일, 날짜, 전체 날짜를 계산하는 유틸 함수 구현 * feat: 하나씩, 범위로 날짜를 선택하는 경우 UI를 구성하는 컴포넌트 구현 - 기존에는 하나의 CalendarDate로 구현을 하려고 했으나, 하나씩 선택하는 것과 범위로 선택하는 것의 UI 책임이 너무 다르다고 판단해 따로 구현하는 것으로 결정 * feat: 달력을 구성하는 각각의 컴포넌트들이 Context API로 공유되는 달력 데이터를 뽑아서 사용할 수 있도록 커스텀 훅 구현 * refactor: 변경된 달력 컴포넌트 반영 * feat: 날짜 추가 정보를 보여주는 컴포넌트 구현 * refactor: 날짜 추가 정보를 보여주는 컴포넌트를 사용하는 것으로 수정 * chore: Fragment 제거, 리액트 모듈 import 구문 제거 * chore: 현재 날짜 정보를 구할 때, 더 의미있는 변수명을 사용하는 것으로 개선 * design: 특정 날짜의 상태를 구하기 위해서 연속된 if문을 사용하는 것이 아닌 객체를 활용해서 구하는 것으로 수정 * feat: 유효한 배열의 타입인지 확인하는 타입 가드 함수 구현 * chore: 날짜 아래에 위치하는 텍스트 컴포넌트명을 더 의미있게 수정 * chore: 현재 달의 상태를 나타내는 변수명 수정 사항 반영, 날짜 상태 props 순서 수정 * chore: 달(Month) 상태 타입 네이밍 수정 * chore: 달력 정보를 Context API를 활용해서 내려주는 프로바이더 컴포넌트 위치 변경 반영 * chore: 달력 정보를 Context API를 활용해서 내려주는 프로바이더 컴포넌트 위치 변경 반영 * chore: 유효한 배열의 타입인지 확인하는 타입 가드 함수 위치 변경 반영 * refactor: 현재 달(Month)의 상태를 나타내는 로직을 함수로 분리 * chore: 함수 개행 추가 * chore: aria-label을 추가하기 위한 TabButton 컴포넌트 props 타입 변경 * chore: type import 구문 컨벤션에 맞게 수정 * chore: 실수로 지웠던 HTML Entities 복구 * chore: 필요없는 날짜 계산 로직 제거
- Loading branch information
Showing
37 changed files
with
1,906 additions
and
347 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { useState } from 'react'; | ||
|
||
import Calendar from './index'; | ||
|
||
const meta = { | ||
title: 'Components/Calendar', | ||
component: Calendar, | ||
tags: ['autodocs'], | ||
|
||
parameters: { | ||
layout: 'centered', | ||
}, | ||
argTypes: { | ||
hasDate: { | ||
description: '선택된 날짜들', | ||
type: 'function', | ||
control: { | ||
disable: true, | ||
}, | ||
}, | ||
onDateClick: { | ||
description: '선택된 날짜 리스트에 특정 날짜를 추가하거나 제거할 수 있는 함수', | ||
}, | ||
}, | ||
decorators: [ | ||
(Story, context) => { | ||
const [selectedDates, setSelectedDates] = useState<string[]>([]); | ||
|
||
const hasDate = (date: string) => selectedDates.includes(date); | ||
|
||
const handleDateClick = (date: string) => { | ||
setSelectedDates((prevDates) => | ||
hasDate(date) ? prevDates.filter((d) => d !== date) : [...prevDates, date], | ||
); | ||
}; | ||
|
||
return ( | ||
<Story | ||
args={{ | ||
...context.args, | ||
hasDate, | ||
onDateClick: handleDateClick, | ||
}} | ||
/> | ||
); | ||
}, | ||
], | ||
} satisfies Meta<typeof Calendar>; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Playground: Story = { | ||
args: { | ||
hasDate: () => false, | ||
onDateClick: () => {}, | ||
}, | ||
render: (args) => { | ||
return <Calendar hasDate={args.hasDate} onDateClick={args.onDateClick} />; | ||
}, | ||
}; | ||
export default {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { css } from '@emotion/react'; | ||
|
||
import theme from '@styles/theme'; | ||
|
||
import CALENDAR_PROPERTIES from '@constants/calendar'; | ||
|
||
export const s_calendarContainer = css` | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
`; | ||
|
||
export const s_calendarContent = css` | ||
display: grid; | ||
grid-template-columns: repeat(7, 1fr); | ||
width: 100%; | ||
`; | ||
|
||
export const s_dayOfWeekContainer = css` | ||
margin-bottom: 2rem; | ||
`; | ||
|
||
export const s_baseDayOfWeek = css` | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
width: 100%; | ||
min-width: 4rem; | ||
height: 4rem; | ||
min-height: 4rem; | ||
${theme.typography.bodyMedium} | ||
`; | ||
|
||
export const s_dayOfWeek = (index: number) => { | ||
if (index === CALENDAR_PROPERTIES.sundayNumber) | ||
return css` | ||
${DAY_SLOT_TEXT_STYLES.holiday} | ||
`; | ||
|
||
if (index === CALENDAR_PROPERTIES.saturdayNumber) | ||
return css` | ||
${DAY_SLOT_TEXT_STYLES.saturday} | ||
`; | ||
|
||
return css` | ||
${DAY_SLOT_TEXT_STYLES.default} | ||
`; | ||
}; | ||
|
||
export const s_monthHeader = css` | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
width: 100%; | ||
margin-bottom: 2rem; | ||
padding: 0 1rem; | ||
`; | ||
|
||
export const s_monthNavigation = css` | ||
cursor: pointer; | ||
background-color: transparent; | ||
border: none; | ||
${theme.typography.titleMedium} | ||
&:disabled { | ||
color: ${theme.colors.grey.primary}; | ||
} | ||
`; | ||
|
||
const DAY_SLOT_TEXT_STYLES = { | ||
selected: css` | ||
color: ${theme.colors.calendar.color.selected}; | ||
`, | ||
holiday: css` | ||
color: ${theme.colors.calendar.color.holiday}; | ||
`, | ||
today: css` | ||
color: ${theme.colors.calendar.color.today}; | ||
`, | ||
saturday: css` | ||
color: #8c9eff; | ||
`, | ||
prevDay: css` | ||
color: ${theme.colors.grey.primary}; | ||
`, | ||
default: css` | ||
color: ${theme.colors.black}; | ||
`, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import { css } from '@emotion/react'; | ||
import type { FlagObject } from 'types/utility'; | ||
|
||
import theme from '@styles/theme'; | ||
|
||
export const s_dateContainer = css` | ||
width: 100%; | ||
min-width: 4.8rem; | ||
height: 4.8rem; | ||
`; | ||
|
||
export const s_baseDateButton = () => css` | ||
cursor: pointer; | ||
position: relative; | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
justify-content: center; | ||
border: none; | ||
&:disabled { | ||
cursor: default; | ||
} | ||
`; | ||
|
||
export const s_singleDateButton = (isSelectedDate: boolean) => css` | ||
background-color: ${isSelectedDate ? theme.colors.pink.medium : 'transparent'}; | ||
border-radius: 0.8rem; | ||
`; | ||
|
||
export const s_rangeDateButton = (isSelectedDate: boolean) => css` | ||
background-color: ${isSelectedDate ? theme.colors.pink.light : 'transparent'}; | ||
`; | ||
|
||
export const s_baseDaySlotText = css` | ||
${theme.typography.bodyLight} | ||
`; | ||
|
||
type DaySlotStatus = | ||
| 'isSelectedFullDate' | ||
| 'isPrevDate' | ||
| 'isSunday' | ||
| 'isSaturday' | ||
| 'isHoliday' | ||
| 'isToday'; | ||
|
||
export const s_daySlotText = ({ | ||
isSelectedFullDate, | ||
isPrevDate, | ||
isSunday, | ||
isSaturday, | ||
isHoliday, | ||
isToday, | ||
}: FlagObject<DaySlotStatus>) => { | ||
if (isSelectedFullDate) return DAY_SLOT_TEXT_STYLES.selected; | ||
if (isHoliday) return DAY_SLOT_TEXT_STYLES.holiday; | ||
if (isPrevDate) return DAY_SLOT_TEXT_STYLES.prevDay; | ||
if (isToday) return DAY_SLOT_TEXT_STYLES.today; | ||
if (isSunday) return DAY_SLOT_TEXT_STYLES.holiday; | ||
if (isSaturday) return DAY_SLOT_TEXT_STYLES.saturday; | ||
|
||
return DAY_SLOT_TEXT_STYLES.default; | ||
}; | ||
|
||
const DAY_SLOT_TEXT_STYLES = { | ||
selected: css` | ||
color: ${theme.colors.calendar.color.selected}; | ||
`, | ||
holiday: css` | ||
color: ${theme.colors.calendar.color.holiday}; | ||
`, | ||
today: css` | ||
color: ${theme.colors.calendar.color.today}; | ||
`, | ||
saturday: css` | ||
color: #8c9eff; | ||
`, | ||
prevDay: css` | ||
color: ${theme.colors.grey.primary}; | ||
`, | ||
default: css` | ||
color: ${theme.colors.black}; | ||
`, | ||
}; | ||
|
||
export const s_holidayText = css` | ||
font-size: 1rem; | ||
font-weight: 300; | ||
line-height: 1.2; | ||
`; | ||
|
||
export const s_rangeStart = (isAllRangeSelected: boolean) => css` | ||
background-color: ${theme.colors.pink.medium}; | ||
&::before { | ||
content: ''; | ||
position: absolute; | ||
top: 0; | ||
right: 0; | ||
bottom: 0; | ||
width: 20%; | ||
height: 100%; | ||
background-color: ${isAllRangeSelected ? theme.colors.pink.light : theme.colors.white}; | ||
} | ||
&::after { | ||
content: ''; | ||
position: absolute; | ||
top: 0; | ||
right: 0.4px; | ||
bottom: 0; | ||
width: 20%; | ||
height: 100%; | ||
background-color: ${theme.colors.pink.medium}; | ||
clip-path: polygon(0 0, 100% 50%, 0 100%); | ||
} | ||
`; | ||
|
||
export const s_rangeEnd = (isAllRangeSelected: boolean) => css` | ||
background-color: ${theme.colors.pink.medium}; | ||
&::before { | ||
content: ''; | ||
position: absolute; | ||
top: 0; | ||
bottom: 0; | ||
left: 0; | ||
width: 20%; | ||
height: 100%; | ||
background-color: ${isAllRangeSelected ? theme.colors.pink.light : theme.colors.white}; | ||
} | ||
&::after { | ||
content: ''; | ||
position: absolute; | ||
top: 0; | ||
bottom: 0; | ||
left: 0.4px; | ||
width: 20%; | ||
height: 100%; | ||
background-color: ${theme.colors.pink.medium}; | ||
clip-path: polygon(100% 0, 0 50%, 100% 100%); | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import type { DateInfo } from '@hooks/useCalendarInfo/useCalendar.type'; | ||
import { getDateInfo2 } from '@hooks/useCalendarInfo/useCalendarInfo.utils'; | ||
|
||
import { | ||
s_baseDateButton, | ||
s_baseDaySlotText, | ||
s_dateContainer, | ||
s_daySlotText, | ||
s_holidayText, | ||
s_rangeDateButton, | ||
s_rangeEnd, | ||
s_rangeStart, | ||
} from '../CalendarDate.styles'; | ||
|
||
interface RangeCalendarDateProps { | ||
dateInfo: DateInfo; | ||
hasDate: (date: string) => boolean; | ||
onDateClick: (date: string) => void; | ||
isRangeStart: boolean; | ||
isRangeEnd: boolean; | ||
isAllRangeSelected: boolean; | ||
} | ||
|
||
export default function RangeCalendarDate({ | ||
dateInfo, | ||
hasDate, | ||
onDateClick, | ||
isRangeStart, | ||
isRangeEnd, | ||
isAllRangeSelected, | ||
}: RangeCalendarDateProps) { | ||
const { key, value, status } = dateInfo; | ||
const { | ||
date, | ||
currentFullDate, | ||
isHoliday, | ||
isToday, | ||
isSaturday, | ||
isSunday, | ||
isPrevDate, | ||
holidayName, | ||
} = getDateInfo2(value, new Date()); | ||
const isSelectedFullDate = hasDate(currentFullDate); | ||
|
||
return status === 'currentMonth' ? ( | ||
<button | ||
key={key} | ||
onClick={() => onDateClick(currentFullDate)} | ||
disabled={isPrevDate} | ||
css={[ | ||
s_dateContainer, | ||
s_baseDateButton, | ||
s_rangeDateButton(isSelectedFullDate), | ||
isRangeStart && s_rangeStart(isAllRangeSelected), | ||
isRangeEnd && s_rangeEnd(isAllRangeSelected), | ||
s_daySlotText({ | ||
isSelectedFullDate, | ||
isPrevDate, | ||
isSunday, | ||
isSaturday, | ||
isHoliday, | ||
isToday, | ||
}), | ||
]} | ||
> | ||
<span css={[s_baseDaySlotText]}>{date}</span> | ||
{isRangeStart && <span css={s_holidayText}>시작</span>} | ||
{isRangeEnd && <span css={s_holidayText}>끝</span>} | ||
{!isRangeStart && !isRangeEnd && <span css={s_holidayText}>{holidayName || '\u00A0'}</span>} | ||
</button> | ||
) : ( | ||
<div key={key} css={s_dateContainer}></div> | ||
); | ||
} |
Oops, something went wrong.