diff --git a/frontend/src/assets/images/pen.svg b/frontend/src/assets/images/pen.svg new file mode 100644 index 000000000..fed0ca14e --- /dev/null +++ b/frontend/src/assets/images/pen.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/images/rotate.svg b/frontend/src/assets/images/rotate.svg new file mode 100644 index 000000000..b3243e113 --- /dev/null +++ b/frontend/src/assets/images/rotate.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/Schedules/DateControlButtons/DateControlButtons.styles.ts b/frontend/src/components/Schedules/DateControlButtons/DateControlButtons.styles.ts new file mode 100644 index 000000000..a9f236488 --- /dev/null +++ b/frontend/src/components/Schedules/DateControlButtons/DateControlButtons.styles.ts @@ -0,0 +1,39 @@ +import { css } from '@emotion/react'; + +import theme from '@styles/theme'; + +export const s_datesControlButtonContainer = css` + position: absolute; + top: 0; + + display: flex; + justify-content: space-between; + + width: 100%; + + -webkit-box-pack: justify; +`; + +export const s_datesControlButton = css` + cursor: pointer; + + width: 2.8rem; + height: 2.8rem; + + font-weight: bold; + color: ${theme.colors.primary}; + + background-color: ${theme.colors.white}; + border: 0.1rem solid ${theme.colors.primary}; + border-radius: 50%; + box-shadow: 0 0.4rem 0.4rem rgb(0 0 0 / 10%); + + :disabled { + cursor: not-allowed; + opacity: 0.4; + } + + &:last-of-type { + margin-right: 1rem; + } +`; diff --git a/frontend/src/components/Schedules/DateControlButtons/index.tsx b/frontend/src/components/Schedules/DateControlButtons/index.tsx new file mode 100644 index 000000000..a92040fe2 --- /dev/null +++ b/frontend/src/components/Schedules/DateControlButtons/index.tsx @@ -0,0 +1,26 @@ +import { s_datesControlButton, s_datesControlButtonContainer } from './DateControlButtons.styles'; + +interface DateControlButtons { + decreaseDatePage: () => void; + increaseDatePage: () => void; + isFirstPage: boolean; + isLastPage: boolean; +} + +export default function DateControlButtons({ + decreaseDatePage, + increaseDatePage, + isFirstPage, + isLastPage, +}: DateControlButtons) { + return ( +
+ + +
+ ); +} diff --git a/frontend/src/components/Schedules/ScheduleOverview/ScheduleOverview.styles.ts b/frontend/src/components/Schedules/ScheduleOverview/ScheduleOverview.styles.ts new file mode 100644 index 000000000..74cecfa7e --- /dev/null +++ b/frontend/src/components/Schedules/ScheduleOverview/ScheduleOverview.styles.ts @@ -0,0 +1,14 @@ +import { css } from '@emotion/react'; + +export const s_scheduleOverviewContainer = css` + display: flex; + flex-direction: column; + row-gap: 1.2rem; + margin-bottom: 1.2rem; +`; + +export const s_infoTextContainer = css` + display: flex; + gap: 0.4rem; + align-items: center; +`; diff --git a/frontend/src/components/Schedules/ScheduleOverview/index.tsx b/frontend/src/components/Schedules/ScheduleOverview/index.tsx new file mode 100644 index 000000000..bb684d763 --- /dev/null +++ b/frontend/src/components/Schedules/ScheduleOverview/index.tsx @@ -0,0 +1,43 @@ +import Text from '@components/_common/Text'; + +import Information from '@assets/images/information.svg'; + +import { s_percentage, s_percentageContainer, s_pinkProgressiveBar } from '../Schedules.styles'; +import { s_infoTextContainer, s_scheduleOverviewContainer } from './ScheduleOverview.styles'; + +interface ScheduleOverviewProps { + selectedAttendee: string; +} + +export default function ScheduleOverview({ selectedAttendee }: ScheduleOverviewProps) { + return ( +
+ {selectedAttendee === '' ? ( + + + 약속 참여자들의 일정을 확인하고 있어요 + + ) : ( + + + 님의 일정을 확인하고 있어요 + + )} +
+ + + {selectedAttendee === '' + ? '시간을 클릭하여 참여할 수 있는 참여자들을 확인해 보세요' + : '나의 약속 참여 시간과 비교해 보세요'} + +
+
+
+
+

0%

+

100%

+
+
+
+ ); +} diff --git a/frontend/src/components/Schedules/SchedulePicker/index.tsx b/frontend/src/components/Schedules/SchedulePicker/index.tsx index 990230217..f9a7a5e08 100644 --- a/frontend/src/components/Schedules/SchedulePicker/index.tsx +++ b/frontend/src/components/Schedules/SchedulePicker/index.tsx @@ -1,4 +1,3 @@ -import { css } from '@emotion/react'; import { useContext, useState } from 'react'; import { useParams } from 'react-router-dom'; import type { MeetingDateTime } from 'types/meeting'; @@ -8,23 +7,29 @@ import { TimePickerUpdateStateContext } from '@contexts/TimePickerUpdateStatePro import ScheduleTimeList from '@components/Schedules/ScheduleTableFrame/ScheduleTimeList'; import { Button } from '@components/_common/Buttons/Button'; +import TabButton from '@components/_common/Buttons/TabButton'; +import Text from '@components/_common/Text'; import usePagedTimePick from '@hooks/usePagedTimePick/usePagedTimePick'; import { usePostScheduleMutation } from '@stores/servers/schedule/mutations'; +import Rotate from '@assets/images/rotate.svg'; + +import DateControlButtons from '../DateControlButtons'; import ScheduleDateDayList from '../ScheduleTableFrame/ScheduleDateDayList'; import { s_baseTimeCell, - s_buttonContainer, + s_bottomFixedButtonContainer, s_cellColorBySelected, - s_datesControlButton, - s_datesControlButtonContainer, + s_circleButton, + s_fullButtonContainer, s_relativeContainer, s_scheduleTable, s_scheduleTableBody, s_scheduleTableContainer, s_scheduleTableRow, + s_selectModeButtonsContainer, } from '../Schedules.styles'; import { convertToSchedule, generateSingleScheduleTable } from '../Schedules.util'; @@ -68,7 +73,7 @@ export default function SchedulePicker({ isLastPage, } = usePagedTimePick(availableDates, schedules); - const { mutate: postScheduleMutate } = usePostScheduleMutation(() => + const { mutate: postScheduleMutate, isPending } = usePostScheduleMutation(() => handleToggleIsTimePickerUpdate(), ); @@ -93,73 +98,74 @@ export default function SchedulePicker({ }; return ( -
-
- -

/

- -

시간으로 선택하기

-
- {isMultiPage && ( -
- - + {TIME_SELECT_MODE.unavailable} + + 시간으로 선택하기
- )} -
- - - - - - - {currentTableValue.map((row, rowIndex) => ( - - {row.map((isSelected, columnIndex) => { - const isHalfHour = rowIndex % 2 !== 0; - const isLastRow = rowIndex === schedules.length - 1; - - return ( - - ); - })} - - ))} - -
-
-
- + {isMultiPage && ( + + )} +
+ + + + + + + {currentTableValue.map((row, rowIndex) => ( + + {row.map((isSelected, columnIndex) => { + const isHalfHour = rowIndex % 2 !== 0; + const isLastRow = rowIndex === schedules.length - 1; + + return ( + + ); + })} + + ))} + +
+
-
+ + ); } diff --git a/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx b/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx index 29381471f..b722a515b 100644 --- a/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx +++ b/frontend/src/components/Schedules/ScheduleViewer/SchedulesViewer.tsx @@ -1,27 +1,32 @@ -import React, { useContext } from 'react'; +import { useContext } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import type { MeetingDateTime } from 'types/meeting'; import { AuthContext } from '@contexts/AuthProvider'; import { TimePickerUpdateStateContext } from '@contexts/TimePickerUpdateStateProvider'; -import { - s_attendeesContainer, - s_tabButton, -} from '@pages/MeetingTimePickPage/MeetingTimePickPage.styles'; +import { Button } from '@components/_common/Buttons/Button'; +import TabButton from '@components/_common/Buttons/TabButton'; import useSelectSchedule from '@hooks/useSelectSchedule/useSelectSchedule'; +import Check from '@assets/images/attendeeCheck.svg'; +import Pen from '@assets/images/pen.svg'; + +import DateControlButtons from '../DateControlButtons'; +import ScheduleOverview from '../ScheduleOverview'; import { - s_buttonContainer, - s_datesControlButton, - s_datesControlButtonContainer, + s_attendeesContainer, + s_bottomFixedButtonContainer, + s_circleButton, + s_fullButtonContainer, s_relativeContainer, } from '../Schedules.styles'; import ScheduleTable from './ScheduleTable'; interface SchedulesViewerProps extends MeetingDateTime { isLocked: boolean; + hostName: string; meetingAttendees: string[]; } @@ -29,6 +34,7 @@ export default function SchedulesViewer({ isLocked, firstTime, lastTime, + hostName, availableDates, meetingAttendees, }: SchedulesViewerProps) { @@ -38,7 +44,7 @@ export default function SchedulesViewer({ const navigate = useNavigate(); const { handleToggleIsTimePickerUpdate } = useContext(TimePickerUpdateStateContext); - const { isLoggedIn } = useContext(AuthContext).state; + const { isLoggedIn, userName } = useContext(AuthContext).state; const { currentDates, @@ -64,38 +70,35 @@ export default function SchedulesViewer({ return ( <>
- + {meetingAttendees.map((attendee) => ( - + ))}
+
- {/* 버튼 관련 스타일은 다음 이슈에서 해결 예정(@해리) */} {isMultiPage && ( -
- - -
+ )}
-
- + ) : ( + + )} +
+ - + ); } diff --git a/frontend/src/components/Schedules/ScheduleViewer/SingleSchedule.tsx b/frontend/src/components/Schedules/ScheduleViewer/SingleSchedule.tsx index 545169c82..e8fbd1e34 100644 --- a/frontend/src/components/Schedules/ScheduleViewer/SingleSchedule.tsx +++ b/frontend/src/components/Schedules/ScheduleViewer/SingleSchedule.tsx @@ -1,8 +1,7 @@ -import React from 'react'; import type { MeetingDateTime } from 'types/meeting'; import type { MeetingSingleSchedule } from 'types/schedule'; -import { s_baseTimeCell, s_cellColorBySelected, s_scheduleTableRow } from '../Schedules.styles'; +import { s_baseTimeCell, s_scheduleTableRow, s_singleCellColor } from '../Schedules.styles'; import { generateSingleScheduleTable } from '../Schedules.util'; interface SingleScheduleProps extends MeetingDateTime { @@ -33,7 +32,7 @@ export default function SingleSchedule({ return ( ); })} diff --git a/frontend/src/components/Schedules/Schedules.styles.ts b/frontend/src/components/Schedules/Schedules.styles.ts index 3badbf077..3d3321b40 100644 --- a/frontend/src/components/Schedules/Schedules.styles.ts +++ b/frontend/src/components/Schedules/Schedules.styles.ts @@ -9,6 +9,15 @@ export const s_container = css` export const s_relativeContainer = css` position: relative; + flex: 1; + max-height: fit-content; +`; + +export const s_selectModeButtonsContainer = css` + display: flex; + gap: 0.4rem; + align-items: center; + margin-bottom: 1.2rem; `; export const s_scheduleTableContainer = css` @@ -21,6 +30,15 @@ export const s_scheduleTableContainer = css` } `; +export const s_attendeesContainer = css` + display: flex; + flex-wrap: wrap; + gap: 1.2rem 1.2rem; + + width: 100%; + margin-bottom: 1.2rem; +`; + export const s_scheduleTable = css` position: relative; @@ -61,10 +79,20 @@ export const s_cellColorByRatio = (ratio: number) => css` background-color: ${ratio > 0 ? getColorByRatio(ratio) : '#f4f4f5'}; `; -export const s_cellColorBySelected = (isSelected: number) => css` +export const s_singleCellColor = (isSelected: number) => css` background-color: ${isSelected ? theme.colors.primary : '#f4f4f5'}; `; +export const s_cellColorBySelected = (isSelected: number, unavailableMode = false) => css` + background-color: ${unavailableMode + ? isSelected + ? theme.colors.pink.deepDark + : '#f4f4f5' + : isSelected + ? theme.colors.green.deep + : '#f4f4f5'}; +`; + export const s_baseTimeCell = (isHalfHour: boolean, isLastRow: boolean) => css` flex: 1; @@ -79,41 +107,31 @@ export const s_baseTimeCell = (isHalfHour: boolean, isLastRow: boolean) => css` `} `; -export const s_datesControlButtonContainer = css` - position: absolute; - z-index: 2; - top: 4.2rem; +export const s_bottomFixedButtonContainer = css` + position: sticky; /* 절대 위치로 부모 컨테이너 내에서 배치 */ + bottom: 0; + left: 0; display: flex; + gap: 1.6rem; + align-items: center; justify-content: space-between; - width: 100%; - - -webkit-box-pack: justify; + /* + position : sticky는 문서의 흐름에 영향을 받기 때문에 부모 태그의 padding 스타일 속성을 상속받게 됨 + 따라서, 부모의 padding인 0 1.6rem을 무시하는 스타일 속성 추가 (@해리) + */ + width: calc(100% + 1.6rem * 2); + height: 6rem; + margin: 1.6rem 0 0 -1.6rem; + padding: 0 1.6rem; + + background-color: #fff; + box-shadow: 0 -4px 4px rgb(0 0 0 / 25%); `; -export const s_datesControlButton = css` - cursor: pointer; - - width: 2.8rem; - height: 2.8rem; - - font-weight: bold; - color: ${theme.colors.primary}; - - background-color: ${theme.colors.white}; - border: 0.1rem solid ${theme.colors.primary}; - border-radius: 50%; - box-shadow: 0 0.4rem 0.4rem rgb(0 0 0 / 10%); - - :disabled { - cursor: not-allowed; - opacity: 0.4; - } - - &:last-of-type { - margin-right: 5rem; - } +export const s_fullButtonContainer = css` + flex: 1; `; export const s_buttonContainer = css` @@ -157,10 +175,31 @@ export const s_percentageContainer = css` display: flex; justify-content: space-between; width: 100%; - margin-bottom: 1.2rem; + margin-top: 0.2rem; `; export const s_percentage = css` ${theme.typography.captionMedium} color: #d4d4d8 `; + +export const s_circleButton = css` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + width: 4.8rem; + height: 4.8rem; + + color: ${theme.colors.primary}; + + background-color: transparent; + border: 1px solid ${theme.colors.primary}; + border-radius: 50%; + box-shadow: 0 4px 4px rgb(0 0 0 / 25%); + + &:disabled { + opacity: 0.3; + } +`; diff --git a/frontend/src/components/_common/Buttons/Button/index.tsx b/frontend/src/components/_common/Buttons/Button/index.tsx index 59141b98c..746c7471e 100644 --- a/frontend/src/components/_common/Buttons/Button/index.tsx +++ b/frontend/src/components/_common/Buttons/Button/index.tsx @@ -1,6 +1,8 @@ import type { SerializedStyles } from '@emotion/react'; import type { ButtonHTMLAttributes, ReactNode } from 'react'; +import Spinner from '@components/_common/Spinner'; + import { s_baseButton, s_size, s_variant } from './Button.styles'; export type ButtonSize = 'xs' | 's' | 'm' | 'full'; @@ -11,18 +13,20 @@ interface ButtonProps extends ButtonHTMLAttributes { size: ButtonSize; borderRadius?: number | string; variant?: ButtonVariant; + isLoading?: boolean; customCss?: SerializedStyles; } export function Button({ - variant, - borderRadius = '0.8rem', children, size, + borderRadius = '0.8rem', + variant, disabled, + isLoading = false, + customCss, type = 'button', onClick, - customCss, }: ButtonProps) { const cssProps = [s_baseButton(borderRadius), s_size(size)]; @@ -30,7 +34,8 @@ export function Button({ return ( ); } diff --git a/frontend/src/components/_common/Spinner/Spinner.styles.ts b/frontend/src/components/_common/Spinner/Spinner.styles.ts new file mode 100644 index 000000000..51ab9e3d2 --- /dev/null +++ b/frontend/src/components/_common/Spinner/Spinner.styles.ts @@ -0,0 +1,111 @@ +import { css, keyframes } from '@emotion/react'; +import type { CSSProperties } from 'react'; + +const pulse = keyframes` + 0%, + 100% { + transform: scale(0); + opacity: 0.5; + } + + 50% { + transform: scale(1); + opacity: 1; + } +`; + +export const s_spinnerContainer = css` + position: relative; + + display: flex; + align-items: center; + justify-content: flex-start; + + width: 2rem; + height: 2rem; +`; + +export const s_spinner = (backgroundColor: CSSProperties['color']) => css` + position: absolute; + top: 0; + left: 0; + + display: flex; + align-items: center; + justify-content: flex-start; + + width: 100%; + height: 100%; + + &::before { + content: ''; + + transform: scale(0); + + width: 20%; + height: 20%; + + opacity: 0.5; + background-color: ${backgroundColor}; + border-radius: 50%; + box-shadow: 0 0 20px rgb(18 31 53 / 30%); + + animation: ${pulse} calc(0.9s * 1.111) ease-in-out infinite; + } + + &:nth-of-type(2) { + transform: rotate(45deg); + } + + &:nth-of-type(2)::before { + animation-delay: calc(0.9s * -0.875); + } + + &:nth-of-type(3) { + transform: rotate(90deg); + } + + &:nth-of-type(3)::before { + animation-delay: calc(0.9s * -0.75); + } + + &:nth-of-type(4) { + transform: rotate(135deg); + } + + &:nth-of-type(4)::before { + animation-delay: calc(0.9s * -0.625); + } + + &:nth-of-type(5) { + transform: rotate(180deg); + } + + &:nth-of-type(5)::before { + animation-delay: calc(0.9s * -0.5); + } + + &:nth-of-type(6) { + transform: rotate(225deg); + } + + &:nth-of-type(6)::before { + animation-delay: calc(0.9s * -0.375); + } + + &:nth-of-type(7) { + transform: rotate(270deg); + } + + &:nth-of-type(7)::before { + animation-delay: calc(0.9s * -0.25); + } + + &:nth-of-type(8) { + transform: rotate(315deg); + } + + &:nth-of-type(8)::before { + animation-delay: calc(0.9s * -0.125); + } +`; diff --git a/frontend/src/components/_common/Spinner/index.tsx b/frontend/src/components/_common/Spinner/index.tsx new file mode 100644 index 000000000..bd1d3aa2f --- /dev/null +++ b/frontend/src/components/_common/Spinner/index.tsx @@ -0,0 +1,22 @@ +import type { CSSProperties } from 'react'; + +import { s_spinner, s_spinnerContainer } from './Spinner.styles'; + +interface SpinnerProps { + backgroundColor: CSSProperties['color']; +} + +export default function Spinner({ backgroundColor }: SpinnerProps) { + return ( +
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/frontend/src/components/_common/Text/Accent/index.tsx b/frontend/src/components/_common/Text/Accent/index.tsx index 075eb4083..c22f45edf 100644 --- a/frontend/src/components/_common/Text/Accent/index.tsx +++ b/frontend/src/components/_common/Text/Accent/index.tsx @@ -1,10 +1,9 @@ -import type { PropsWithChildren } from 'react'; - - import { s_accentTextStyle } from './Accent.styles'; -interface AccentProps extends PropsWithChildren {} +interface AccentProps { + text: string; +} -export default function Accent({ children }: AccentProps) { - return {children}; +export default function Accent({ text }: AccentProps) { + return {text}; } diff --git a/frontend/src/components/_common/Text/Text.stories.tsx b/frontend/src/components/_common/Text/Text.stories.tsx index 65d573737..4e0bc79a8 100644 --- a/frontend/src/components/_common/Text/Text.stories.tsx +++ b/frontend/src/components/_common/Text/Text.stories.tsx @@ -65,7 +65,7 @@ export const CaptionText: Story = { export const WelcomeText: Story = { render: () => ( - 페드로님 반가워요 👋🏻 + 님 반가워요 👋🏻 ), }; @@ -73,7 +73,7 @@ export const WelcomeText: Story = { export const TitleText: Story = { render: () => ( - 모모 런칭데이 회식 + 약속 참여자들이 선택한 시간대를 알려드릴게요 ), diff --git a/frontend/src/components/_common/Text/Text.styles.ts b/frontend/src/components/_common/Text/Text.styles.ts index 0e1bbe627..8c341ff37 100644 --- a/frontend/src/components/_common/Text/Text.styles.ts +++ b/frontend/src/components/_common/Text/Text.styles.ts @@ -36,6 +36,7 @@ export const s_textStyles = ({ }) => { return css` color: ${TEXT_COLOR_STYLES[variant]}; + white-space: pre-line; vertical-align: middle; ${TEXT_TYPOGRAPHIES[typo]} `; diff --git a/frontend/src/layouts/GlobalLayout.styles.ts b/frontend/src/layouts/GlobalLayout.styles.ts index 039f34e7e..887e4d9c2 100644 --- a/frontend/src/layouts/GlobalLayout.styles.ts +++ b/frontend/src/layouts/GlobalLayout.styles.ts @@ -15,5 +15,5 @@ export const s_globalContainer = css` export const s_content = css` flex: 1; - padding: 2.4rem 1.6rem; + padding: 2.4rem 1.6rem 0; `; diff --git a/frontend/src/pages/LandingPage/index.tsx b/frontend/src/pages/LandingPage/index.tsx index 66cac917e..2c7e27be7 100644 --- a/frontend/src/pages/LandingPage/index.tsx +++ b/frontend/src/pages/LandingPage/index.tsx @@ -15,7 +15,7 @@ export default function LandingPage() { return (
- 모두 쉽게 모이자! 모모 🍑 + 모두 쉽게 모이자! 🍑 @@ -93,11 +97,6 @@ export default function MeetingTimePickPage() { /> ) )} - {!isTimePickerUpdate && ( - - )}
); } diff --git a/frontend/src/pages/NotFoundPage/index.tsx b/frontend/src/pages/NotFoundPage/index.tsx index 77167b106..a6f220087 100644 --- a/frontend/src/pages/NotFoundPage/index.tsx +++ b/frontend/src/pages/NotFoundPage/index.tsx @@ -17,7 +17,7 @@ export default function NotFoundPage() {
- 404 + 요청하신 페이지를 찾을 수 없어요 :(
diff --git a/frontend/src/styles/global.ts b/frontend/src/styles/global.ts index 86d44f96a..2099e383e 100644 --- a/frontend/src/styles/global.ts +++ b/frontend/src/styles/global.ts @@ -109,7 +109,7 @@ const globalStyles = css` font-family: 'Spoqa Han Sans Neo', sans-serif; font-size: 1.6rem; - background-color: #6b6666; + background-color: #fff; } article,