From 6d9d1a594f6298eabb027d28bd735df166cdbedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Thu, 21 Sep 2023 20:45:30 +0900 Subject: [PATCH] =?UTF-8?q?Feat/#431=20=EB=85=B8=EB=9E=98=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=EC=8A=A4?= =?UTF-8?q?=EC=99=80=EC=9D=B4=ED=94=84=20=EC=BD=94=EC=B9=98=20=EB=A7=88?= =?UTF-8?q?=ED=81=AC=EB=A5=BC=20=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4=20(#4?= =?UTF-8?q?32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: CoachMark 컴포넌트 구현 - 기존 Modal 컴포넌트와 매우 비슷함 - backdrop에 onClick 이벤트로 있던 closeModal이 없음 - Container의 backbround-color와 border 삭제 * feat: Swipe 설명을 위한 코치마크 적용 * feat: Modal 컴포넌트의 Container에 css를 추가할 수 있도록 변경 * refactor: CoachMark 컴포넌트를 Modal 컴포넌트로 대체 * fix: styled-component $ 사인 추가 * feat: useLocalStorage 훅 구현 * feat: 노래 상세페이지에 처음 접속할 때만 코치마크를 보여주도록 변경 --- frontend/src/assets/icon/swipe-up-down.svg | 1 + frontend/src/pages/SongDetailListPage.tsx | 80 ++++++++++++++----- .../src/shared/components/Modal/Modal.tsx | 14 +++- frontend/src/shared/hooks/useLocalStorage.ts | 31 +++++++ 4 files changed, 101 insertions(+), 25 deletions(-) create mode 100644 frontend/src/assets/icon/swipe-up-down.svg create mode 100644 frontend/src/shared/hooks/useLocalStorage.ts diff --git a/frontend/src/assets/icon/swipe-up-down.svg b/frontend/src/assets/icon/swipe-up-down.svg new file mode 100644 index 000000000..d6029a284 --- /dev/null +++ b/frontend/src/assets/icon/swipe-up-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/pages/SongDetailListPage.tsx b/frontend/src/pages/SongDetailListPage.tsx index ec582fd7f..50baef3f4 100644 --- a/frontend/src/pages/SongDetailListPage.tsx +++ b/frontend/src/pages/SongDetailListPage.tsx @@ -1,18 +1,26 @@ import { useEffect, useLayoutEffect, useRef } from 'react'; import { styled } from 'styled-components'; +import swipeUpDown from '@/assets/icon/swipe-up-down.svg'; import SongDetailItem from '@/features/songs/components/SongDetailItem'; import { getExtraNextSongDetails, getExtraPrevSongDetails, getSongDetailEntries, } from '@/features/songs/remotes/songs'; +import useModal from '@/shared/components/Modal/hooks/useModal'; +import Modal from '@/shared/components/Modal/Modal'; +import Spacing from '@/shared/components/Spacing'; import useExtraFetch from '@/shared/hooks/useExtraFetch'; import useFetch from '@/shared/hooks/useFetch'; +import useLocalStorage from '@/shared/hooks/useLocalStorage'; import useValidParams from '@/shared/hooks/useValidParams'; import createObserver from '@/shared/utils/createObserver'; import type { Genre } from '@/features/songs/types/Song.type'; const SongDetailListPage = () => { + const { isOpen, closeModal } = useModal(true); + const [onboarding, setOnboarding] = useLocalStorage('onboarding', true); + const { id: songIdParams, genre: genreParams } = useValidParams(); const { data: songDetailEntries } = useFetch(() => getSongDetailEntries(Number(songIdParams), genreParams as Genre) @@ -49,6 +57,11 @@ const SongDetailListPage = () => { return Number(lastSongId); }; + const closeCoachMark = () => { + setOnboarding(false); + closeModal(); + }; + useEffect(() => { if (!prevTargetRef.current) return; @@ -80,27 +93,42 @@ const SongDetailListPage = () => { const { prevSongs, currentSong, nextSongs } = songDetailEntries; return ( - - + <> + {onboarding && ( + + + + +
위 아래로 스와이프하여 노래를 탐색해보세요!
+ + + 확인 + +
+ )} + + + + ); }; @@ -141,3 +169,13 @@ export const ItemContainer = styled.div` } } `; + +const Confirm = styled.button` + width: 200px; + height: 40px; + + color: ${({ theme: { color } }) => color.black}; + + background-color: ${({ theme: { color } }) => color.white}; + border-radius: 10px; +`; diff --git a/frontend/src/shared/components/Modal/Modal.tsx b/frontend/src/shared/components/Modal/Modal.tsx index 491fc269d..be0ac8f62 100644 --- a/frontend/src/shared/components/Modal/Modal.tsx +++ b/frontend/src/shared/components/Modal/Modal.tsx @@ -2,14 +2,16 @@ import { useCallback, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { styled } from 'styled-components'; import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react'; +import type { CSSProp } from 'styled-components'; interface ModalProps extends HTMLAttributes { isOpen: boolean; closeModal: () => void; children: ReactElement | ReactElement[]; + css?: CSSProp; } -const Modal = ({ isOpen, closeModal, children }: PropsWithChildren) => { +const Modal = ({ isOpen, closeModal, children, css }: PropsWithChildren) => { const closeByEsc = useCallback( ({ key }: KeyboardEvent) => { if (key === 'Escape') { @@ -36,7 +38,9 @@ const Modal = ({ isOpen, closeModal, children }: PropsWithChildren) {isOpen && ( )} , @@ -58,10 +62,10 @@ export const Backdrop = styled.div` margin: 0; padding: 0; - background-color: rgba(0, 0, 0, 0.5); + background-color: rgba(0, 0, 0, 0.7); `; -export const Container = styled.div` +export const Container = styled.div<{ $css: CSSProp }>` position: fixed; top: 50%; transform: translateY(-50%); @@ -80,6 +84,8 @@ export const Container = styled.div` background-color: #17171c; border: none; border-radius: 16px; + + ${(props) => props.$css} `; export const Wrapper = styled.div` diff --git a/frontend/src/shared/hooks/useLocalStorage.ts b/frontend/src/shared/hooks/useLocalStorage.ts new file mode 100644 index 000000000..f4d3ced3b --- /dev/null +++ b/frontend/src/shared/hooks/useLocalStorage.ts @@ -0,0 +1,31 @@ +import { useState } from 'react'; + +const useLocalStorage = ( + key: string, + initialValue: T +): [localStorageValue: T, setLocalStorageValue: (newValue: T) => void] => { + const [localStorageValue, setLocalStorageValue] = useState(() => { + try { + const value = localStorage.getItem(key); + + if (value) { + return JSON.parse(value); + } else { + localStorage.setItem(key, JSON.stringify(initialValue)); + return initialValue; + } + } catch (error) { + localStorage.setItem(key, JSON.stringify(initialValue)); + return initialValue; + } + }); + + const setLocalStorageStateValue = (newValue: T) => { + localStorage.setItem(key, JSON.stringify(newValue)); + setLocalStorageValue(newValue); + }; + + return [localStorageValue, setLocalStorageStateValue]; +}; + +export default useLocalStorage;