Skip to content

Commit

Permalink
Feat/#431 노래 상세 페이지에 스와이프 코치 마크를 구현한다 (#432)
Browse files Browse the repository at this point in the history
* 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: 노래 상세페이지에 처음 접속할 때만 코치마크를 보여주도록 변경
  • Loading branch information
cruelladevil authored Sep 21, 2023
1 parent 15c6dbd commit 6d9d1a5
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 25 deletions.
1 change: 1 addition & 0 deletions frontend/src/assets/icon/swipe-up-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 59 additions & 21 deletions frontend/src/pages/SongDetailListPage.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>('onboarding', true);

const { id: songIdParams, genre: genreParams } = useValidParams();
const { data: songDetailEntries } = useFetch(() =>
getSongDetailEntries(Number(songIdParams), genreParams as Genre)
Expand Down Expand Up @@ -49,6 +57,11 @@ const SongDetailListPage = () => {
return Number(lastSongId);
};

const closeCoachMark = () => {
setOnboarding(false);
closeModal();
};

useEffect(() => {
if (!prevTargetRef.current) return;

Expand Down Expand Up @@ -80,27 +93,42 @@ const SongDetailListPage = () => {
const { prevSongs, currentSong, nextSongs } = songDetailEntries;

return (
<ItemContainer>
<ObservingTrigger ref={prevTargetRef} aria-hidden="true" />

{extraPrevSongDetails?.map((extraPrevSongDetail) => (
<SongDetailItem key={extraPrevSongDetail.id} {...extraPrevSongDetail} />
))}
{prevSongs.map((prevSongDetail) => (
<SongDetailItem key={prevSongDetail.id} {...prevSongDetail} />
))}

<SongDetailItem ref={itemRef} key={currentSong.id} {...currentSong} />

{nextSongs.map((nextSongDetail) => (
<SongDetailItem key={nextSongDetail.id} {...nextSongDetail} />
))}
{extraNextSongDetails?.map((extraNextSongDetail) => (
<SongDetailItem key={extraNextSongDetail.id} {...extraNextSongDetail} />
))}

<ObservingTrigger ref={nextTargetRef} aria-hidden="true" />
</ItemContainer>
<>
{onboarding && (
<Modal isOpen={isOpen} closeModal={closeCoachMark} css={{ backgroundColor: 'transparent' }}>
<Spacing direction="vertical" size={170} />
<img src={swipeUpDown} width="100px" />
<Spacing direction="vertical" size={30} />
<div style={{ fontSize: '18px' }}>위 아래로 스와이프하여 노래를 탐색해보세요!</div>
<Spacing direction="vertical" size={40} />
<Confirm type="button" onClick={closeCoachMark}>
확인
</Confirm>
</Modal>
)}

<ItemContainer>
<ObservingTrigger ref={prevTargetRef} aria-hidden="true" />

{extraPrevSongDetails?.map((extraPrevSongDetail) => (
<SongDetailItem key={extraPrevSongDetail.id} {...extraPrevSongDetail} />
))}
{prevSongs.map((prevSongDetail) => (
<SongDetailItem key={prevSongDetail.id} {...prevSongDetail} />
))}

<SongDetailItem ref={itemRef} key={currentSong.id} {...currentSong} />

{nextSongs.map((nextSongDetail) => (
<SongDetailItem key={nextSongDetail.id} {...nextSongDetail} />
))}
{extraNextSongDetails?.map((extraNextSongDetail) => (
<SongDetailItem key={extraNextSongDetail.id} {...extraNextSongDetail} />
))}

<ObservingTrigger ref={nextTargetRef} aria-hidden="true" />
</ItemContainer>
</>
);
};

Expand Down Expand Up @@ -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;
`;
14 changes: 10 additions & 4 deletions frontend/src/shared/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement> {
isOpen: boolean;
closeModal: () => void;
children: ReactElement | ReactElement[];
css?: CSSProp;
}

const Modal = ({ isOpen, closeModal, children }: PropsWithChildren<ModalProps>) => {
const Modal = ({ isOpen, closeModal, children, css }: PropsWithChildren<ModalProps>) => {
const closeByEsc = useCallback(
({ key }: KeyboardEvent) => {
if (key === 'Escape') {
Expand All @@ -36,7 +38,9 @@ const Modal = ({ isOpen, closeModal, children }: PropsWithChildren<ModalProps>)
{isOpen && (
<Wrapper>
<Backdrop role="backdrop" onClick={closeModal} aria-hidden="true" />
<Container role="dialog">{children}</Container>
<Container role="dialog" $css={css}>
{children}
</Container>
</Wrapper>
)}
</>,
Expand All @@ -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%);
Expand All @@ -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`
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/shared/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useState } from 'react';

const useLocalStorage = <T>(
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;

0 comments on commit 6d9d1a5

Please sign in to comment.