diff --git a/packages/web/src/common/components/Calendar/Calendar.tsx b/packages/web/src/common/components/Calendar/Calendar.tsx index 527c235..9659825 100644 --- a/packages/web/src/common/components/Calendar/Calendar.tsx +++ b/packages/web/src/common/components/Calendar/Calendar.tsx @@ -6,12 +6,13 @@ import { addDays, isSameMonth, isSameDay, + getDay, } from "date-fns"; import styled from "styled-components"; import MonthNavigator from "./_atomic/MonthNavigator"; import CalendarWeek, { CalendarSizeProps } from "./_atomic/CalendarWeek"; import { CalendarDateProps } from "./_atomic/CalendarDate"; -import CalendarWeekdays from "./_atomic/CalendarWeekdays"; +import Typography from "../Typography"; interface EventPeriod { start: Date; @@ -20,31 +21,80 @@ interface EventPeriod { interface CalendarProps extends CalendarSizeProps { existDates: Date[]; - eventPeriods: EventPeriod[]; + eventPeriods?: EventPeriod[]; selectedDates: Date[]; onDateClick?: (date: Date) => void; + width?: string; + height?: string; + title?: string; + small?: boolean; } -const CalendarWrapper = styled.div` +const CalendarWrapper = styled.div<{ + width?: CalendarProps["width"]; + height?: CalendarProps["height"]; +}>` display: flex; flex-direction: column; - align-items: center; - gap: 20px; - width: 100%; + width: ${({ width }) => width || "100%"}; + height: ${({ height }) => height || "100%"}; +`; + +const CalendarContentWrapper = styled.div<{ + title?: CalendarProps["title"]; + small?: CalendarProps["small"]; +}>` + display: flex; + height: ${({ small }) => (small ? "calc(100% - 28px)" : "calc(100% - 40px)")}; + height: ${({ title }) => title === "" && "100%"}; + padding: ${({ small }) => (small ? "20px 18px" : "30px")}; + flex-direction: column; + align-items: flex-end; + gap: 12px; + flex-shrink: 0; + border-radius: ${({ title }) => (title !== "" ? "0px 0px 4px 4px" : "4px")}; + ${({ title, theme }) => + title === "" + ? `border: 2px solid ${theme.colors.GRAY[100]}` + : `border-left: 2px solid ${theme.colors.GRAY[100]}; + border-right: 2px solid ${theme.colors.GRAY[100]}; + border-bottom: 2px solid ${theme.colors.GRAY[100]};`}; `; -const WeekWrapper = styled.div` +const WeekWrapper = styled.div<{ + small?: CalendarProps["small"]; +}>` display: flex; flex-direction: column; + gap: auto; + justify-content: space-between; + align-items: flex-start; + align-self: stretch; + flex: 1 0 0; + height: 100%; +`; + +const TitleWrapper = styled.div<{ + small?: CalendarProps["small"]; +}>` + display: flex; + height: ${({ small }) => (small ? "28px" : "40px")}; + border-radius: 4px 4px 0px 0px; + background: ${({ theme }) => theme.colors.GREEN[600]}; + display: flex; + padding: ${({ small }) => (small ? "8px 12px" : "10px 20px")}; align-items: center; - gap: 8px; - width: 100%; + gap: 20px; + align-self: stretch; `; const Calendar: React.FC = ({ - size = "md", + title = "", + width = undefined, + height = undefined, + small = false, existDates, - eventPeriods, + eventPeriods = [], selectedDates, onDateClick = () => {}, }) => { @@ -55,7 +105,7 @@ const Calendar: React.FC = ({ start: startOfMonth(currentDate), end: endOfMonth(currentDate), }, - { weekStartsOn: 0 }, + { weekStartsOn: 1 }, ); const handleDateClick = (date: Date) => { @@ -70,10 +120,16 @@ const Calendar: React.FC = ({ const day = addDays(startDate, index); const isCurrentMonth = isSameMonth(day, currentDate); const exist = existDates.some(existDate => isSameDay(existDate, day)); + const dayInWeek = getDay(day); let type: CalendarDateProps["type"] = isCurrentMonth ? "Default" : "Past/Future"; + if (dayInWeek === 0) { + type = "Sunday"; + } else if (dayInWeek === 6) { + type = "Saturday"; + } if (!isCurrentMonth) { type = "Past/Future"; } else if ( @@ -82,7 +138,12 @@ const Calendar: React.FC = ({ type = "Selected"; } else { eventPeriods.forEach(period => { - if (isSameDay(day, period.start)) { + if ( + isSameDay(period.start, period.end) && + isSameDay(day, period.start) + ) { + type = "Selected"; + } else if (isSameDay(day, period.start)) { type = "Start"; } else if (isSameDay(day, period.end)) { type = "End"; @@ -100,19 +161,37 @@ const Calendar: React.FC = ({ }); return ( - - - - - {weeks.map((weekStart: Date) => ( - - ))} - + + {title !== "" && ( + + {small ? ( + + {title} + + ) : ( + + {title} + + )} + + )} + + + + {weeks.map((weekStart: Date) => ( + + ))} + + ); }; diff --git a/packages/web/src/common/components/Calendar/_atomic/CalendarDate.tsx b/packages/web/src/common/components/Calendar/_atomic/CalendarDate.tsx index 50f6915..b708a4e 100644 --- a/packages/web/src/common/components/Calendar/_atomic/CalendarDate.tsx +++ b/packages/web/src/common/components/Calendar/_atomic/CalendarDate.tsx @@ -5,8 +5,15 @@ import { DefaultTheme } from "styled-components/dist/types"; export interface CalendarDateProps { date: Date; exist: boolean; - type?: "Default" | "Pass" | "Start" | "End" | "Selected" | "Past/Future"; - size?: "lg" | "md" | "sm"; + type?: + | "Default" + | "Saturday" + | "Sunday" + | "Pass" + | "Start" + | "End" + | "Selected" + | "Past/Future"; onDateClick?: (date: Date) => void; } @@ -14,8 +21,16 @@ const getBackgroundColor = ( theme: DefaultTheme, type?: CalendarDateProps["type"], ) => { - if (type === "Default") return theme.colors.PRIMARY; - if (type === "Past/Future") return theme.colors.GRAY[100]; + if ( + type === "Default" || + type === "Saturday" || + type === "Sunday" || + type === "Start" || + type === "End" || + type === "Pass" + ) + return theme.colors.PRIMARY; + if (type === "Past/Future") return theme.colors.GREEN[100]; return theme.colors.WHITE; }; @@ -24,11 +39,12 @@ const ExistWrapper = styled.div<{ type?: CalendarDateProps["type"]; }>` display: flex; + position: relative; + width: 20px; + height: 20px; align-items: center; justify-content: center; - position: relative; - width: 24px; - height: 24px; + flex-direction: column; ${({ exist, type, theme }) => exist && @@ -36,96 +52,69 @@ const ExistWrapper = styled.div<{ &::after { content: ""; position: absolute; - right: 0; - top: 0; + right: -4px; + top: -1px; width: 4px; height: 4px; background-color: ${getBackgroundColor(theme, type)}; - border-radius: 2px; + border: 1px solid ${theme.colors.WHITE}; + border-radius: 3px; } `} `; const DateContainer = styled.div` display: flex; - align-items: center; justify-content: center; - border-radius: 4px; + align-items: center; + align-content: center; + align-self: stretch; font-size: 16px; - font-weight: ${({ theme }) => theme.fonts.WEIGHT.MEDIUM}; + text-align: center; + font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR}; line-height: 20px; font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; - - ${({ size }) => { - switch (size) { - case "sm": - return css` - width: 32px; - height: 32px; - `; - case "md": - return css` - width: 40px; - height: 40px; - `; - case "lg": - default: - return css` - width: 48px; - height: 48px; - `; - } - }} - background-color: ${({ type, theme }) => { - if (type === "Past/Future" || type === "Default") return "transparent"; - if (type === "Pass") return theme.colors.GREEN[300]; - return theme.colors.PRIMARY; - }}; + gap: 10px; + flex: 1 0 0; + flex-wrap: wrap; color: ${({ type, theme }) => { - if (type === "Default") return theme.colors.BLACK; + if (type === "Default") return theme.colors.GRAY[900]; + if (type === "Saturday") return theme.colors.GREEN[700]; + if (type === "Sunday") return theme.colors.RED[700]; if (type === "Past/Future") return theme.colors.GRAY[100]; - return theme.colors.WHITE; + return theme.colors.BLACK; }}; `; const DateWrapper = styled.div<{ type?: CalendarDateProps["type"]; - size?: CalendarDateProps["size"]; }>` display: flex; - align-items: center; justify-content: center; - flex: 1; + align-items: center; + align-content: center; + flex: 1 0 0; + align-self: stretch; + flex-wrap: wrap; + padding: 2px 0px; + height: fit-content; cursor: ${({ onClick }) => (onClick ? "pointer" : "default")}; - width: 100%; - ${({ size }) => { - switch (size) { - case "sm": - return css` - height: 32px; - `; - case "md": - return css` - height: 40px; - `; - case "lg": - default: - return css` - height: 48px; - `; - } - }} - background: ${({ type, theme }) => { - switch (type) { - case "End": - return `linear-gradient(to left, rgba(255, 255, 255, 0) 50%, ${theme.colors.GREEN[300]} 50%)`; - case "Start": - return `linear-gradient(to right, rgba(255, 255, 255, 0) 50%, ${theme.colors.GREEN[300]} 50%)`; - case "Pass": - return `${theme.colors.GREEN[300]}`; - default: - return "transparent"; - } + background-color: ${({ type, theme }) => { + if ( + type === "Past/Future" || + type === "Default" || + type === "Saturday" || + type === "Sunday" + ) + return "transparent"; + if (type === "Pass") return theme.colors.GREEN[100]; + return theme.colors.GREEN[300]; + }}; + border-radius: ${({ type }) => { + if (type === "Start") return "2px 0px 0px 2px"; + if (type === "End") return "0px 2px 2px 0px"; + if (type === "Pass") return "0px"; + return "2px"; }}; `; @@ -133,7 +122,6 @@ const CalendarDate: React.FC = ({ date, exist, type = "Default", - size = "lg", onDateClick = () => {}, }) => { const handleClick = () => { @@ -142,8 +130,8 @@ const CalendarDate: React.FC = ({ } }; return ( - - + + {date.getDate()} diff --git a/packages/web/src/common/components/Calendar/_atomic/CalendarWeek.tsx b/packages/web/src/common/components/Calendar/_atomic/CalendarWeek.tsx index 0f8f617..42e8d1d 100644 --- a/packages/web/src/common/components/Calendar/_atomic/CalendarWeek.tsx +++ b/packages/web/src/common/components/Calendar/_atomic/CalendarWeek.tsx @@ -8,40 +8,57 @@ interface CalendarWeekProps { exist: boolean; type?: CalendarDateProps["type"]; }[]; - size?: CalendarDateProps["size"]; + small?: boolean; onDateClick: (date: Date) => void; } -export interface CalendarSizeProps { - size: CalendarDateProps["size"]; -} +export interface CalendarSizeProps {} const WeekWrapper = styled.div` display: flex; - justify-content: space-evenly; + justify-content: space-between; align-items: center; align-self: stretch; - width: 100%; - flex: 1; + height: fit-content; +`; + +const SmallWeekWrapper = styled.div` + display: flex; + align-items: flex-start; + align-self: stretch; + justify-content: space-between; + height: fit-content; `; const CalendarWeek: React.FC = ({ week, - size = "lg", + small = false, onDateClick, -}) => ( - - {week.map(day => ( - - ))} - -); +}) => + small ? ( + + {week.map(day => ( + + ))} + + ) : ( + + {week.map(day => ( + + ))} + + ); export default CalendarWeek; diff --git a/packages/web/src/common/components/Calendar/_atomic/CalendarWeekdays.tsx b/packages/web/src/common/components/Calendar/_atomic/CalendarWeekdays.tsx index 39d248e..c9bba3d 100644 --- a/packages/web/src/common/components/Calendar/_atomic/CalendarWeekdays.tsx +++ b/packages/web/src/common/components/Calendar/_atomic/CalendarWeekdays.tsx @@ -1,14 +1,9 @@ import React from "react"; -import styled, { css } from "styled-components"; -import { CalendarDateProps } from "./CalendarDate"; +import styled from "styled-components"; -export interface CalendarSizeProps { - size: CalendarDateProps["size"]; -} +export interface CalendarSizeProps {} -const DayWrapper = styled.div<{ - size?: CalendarDateProps["size"]; -}>` +const DayWrapper = styled.div` display: flex; align-items: center; justify-content: center; @@ -19,41 +14,28 @@ const DayWrapper = styled.div<{ font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; color: ${({ theme }) => theme.colors.GRAY[400]}; width: 100%; - ${({ size }) => { - switch (size) { - case "sm": - return css` - height: 32px; - `; - case "md": - return css` - height: 40px; - `; - case "lg": - default: - return css` - height: 48px; - `; - } - }} `; const WeekWrapper = styled.div` display: flex; + flex-direction: column; justify-content: space-evenly; - align-items: center; + align-items: flex-end; + align-self: stretch; + gap: 20px; width: 100%; + flex: 1 0 0; `; -const CalendarWeekdays: React.FC = ({ size }) => ( - - - - - - - - +const CalendarWeekdays = ( + + + + + + + + ); diff --git a/packages/web/src/common/components/Calendar/_atomic/MonthNavigator.tsx b/packages/web/src/common/components/Calendar/_atomic/MonthNavigator.tsx index e6ebb7b..8b9a5ac 100644 --- a/packages/web/src/common/components/Calendar/_atomic/MonthNavigator.tsx +++ b/packages/web/src/common/components/Calendar/_atomic/MonthNavigator.tsx @@ -7,30 +7,48 @@ import Icon from "@sparcs-students/web/common/components/Icon"; interface MonthNavigatorProps { currentDate: Date; onChange: (date: Date) => void; + small?: boolean; } const NavigatorWrapper = styled.div` display: flex; align-items: center; justify-content: space-between; - width: 160px; - font-size: 16px; - line-height: 20px; - font-weight: ${({ theme }) => theme.fonts.WEIGHT.MEDIUM}; - font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; - color: ${({ theme }) => theme.colors.BLACK}; + width: 100%; user-select: none; - gap: 16px; + flex-shrink: 0; + align-self: stretch; `; -const MonthDisplay = styled.div` - flex-grow: 1; +const MonthDisplay = styled.div<{ + small?: MonthNavigatorProps["small"]; +}>` + display: flex; + justify-content: space-between; + align-items: center; + font-size: ${({ small }) => (small ? "24px" : "28px")}; + line-height: ${({ small }) => (small ? "24px" : "30px")}; text-align: center; + font-weight: ${({ small, theme }) => + small ? theme.fonts.WEIGHT.MEDIUM : theme.fonts.WEIGHT.SEMIBOLD}; + font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD}; + color: ${({ theme }) => theme.colors.GRAY[900]}; +`; + +const ChevronBlockWrapper = styled.div` + display: flex; + width: 36px; + height: 36px; + padding: 5px 11px; + justify-content: center; + align-items: center; + gap: 10px; `; const MonthNavigator: React.FC = ({ currentDate, onChange = () => {}, + small = false, }) => { const today = new Date(); @@ -50,11 +68,23 @@ const MonthNavigator: React.FC = ({ return ( - - - {format(currentDate, "yyyy년 M월", { locale: ko })} + + {small ? ( + + ) : ( + + )} + + + {format(currentDate, "yyyy. MM.", { locale: ko })} - + + {small ? ( + + ) : ( + + )} + ); };