Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 범용적인 DropDown 컴포넌트를 celuveat-ui-library 배포 후 적용 #652

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ jobs:
${{ runner.os }}-yarn-

- name: 🍔 yarn install
run: pwd && ls && yarn install
run: rm -rf ./node_modules yarn.lock && yarn cache clean && yarn install
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 오류를 수정한걸까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저번에 yarn.lock을 최신화 할 경우(라이브러리를 다운 받거나 yarn.lock)을 수정할 때 github ci cd에서 빌드 시 오류가 발생하였는데 yarn cache 문제 때문이었습니다. 따라서 이를 해결했습니다!!

working-directory: ${{ env.working-directory }}

- name: 🍔 eslint 테스트
run: pwd && ls && yarn lint
run: yarn lint
working-directory: ${{ env.working-directory }}

- name: 🍔 React 프로젝트 빌드
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@tanstack/react-query-devtools": "^4.36.1",
"axios": "^1.5.1",
"browser-image-compression": "^2.0.2",
"celuveat-ui-library": "^1.2.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0",
Expand Down
52 changes: 17 additions & 35 deletions frontend/src/components/@common/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,34 @@ import { Link, useLocation, useNavigate } from 'react-router-dom';
import { Wrapper } from '@googlemaps/react-wrapper';

import Logo from '~/assets/icons/logo.svg';

import InfoDropDown from '~/components/InfoDropDown';
import LoginModal from '~/components/LoginModal';
import SearchBar from '~/components/SearchBar';

import useBooleanState from '~/hooks/useBooleanState';

import TextButton from '../Button';
import TextButton from '~/components/@common/Button';
import InfoDropDown from '~/components/InfoDropDown/InfoDropDown';

function Header() {
const navigator = useNavigate();
const { pathname } = useLocation();
const { value: isModalOpen, setTrue: openModal, setFalse: closeModal } = useBooleanState(false);

const handleInfoDropDown = (event: React.MouseEvent<HTMLElement>) => {
const currentOption = event.currentTarget.dataset.name;

if (currentOption === '로그인') openModal();
if (currentOption === '마이 페이지') navigator('/user');
if (currentOption === '위시리스트') navigator('/restaurants/like');
if (currentOption === '회원 탈퇴') navigator('/withdrawal');
};

const isMapPage = pathname === '/map';

return (
<>
<StyledHeader>
<Link aria-label="셀럽잇 홈페이지" role="button" to="/">
<Logo width={136} />
</Link>
{isMapPage && (
<Wrapper apiKey={process.env.GOOGLE_MAP_API_KEY} language="ko" libraries={['places']}>
<SearchBar />
</Wrapper>
<StyledHeader>
<Link aria-label="셀럽잇 홈페이지" role="button" to="/">
<Logo width={136} />
</Link>
{isMapPage && (
<Wrapper apiKey={process.env.GOOGLE_MAP_API_KEY} language="ko" libraries={['places']}>
<SearchBar />
</Wrapper>
)}
<StyledRightSection>
{!isMapPage && (
<TextButton text="지도로 보기" colorType="dark" type="button" onClick={() => navigator('/map')} />
)}
<StyledRightSection>
{!isMapPage && (
<TextButton text="지도로 보기" colorType="dark" type="button" onClick={() => navigator('/map')} />
)}
<InfoDropDown externalOnClick={handleInfoDropDown} isOpen={isModalOpen} label="로그인" />
</StyledRightSection>
</StyledHeader>

<LoginModal close={closeModal} isOpen={isModalOpen} />
</>
<InfoDropDown />
</StyledRightSection>
</StyledHeader>
);
}

Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/@common/Map/Marker/Marker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import styled, { css, keyframes } from 'styled-components';
import { memo } from 'react';
import Love from '~/assets/icons/love.svg';
import ProfileImage from '../../ProfileImage';
import { Restaurant } from '~/@types/restaurant.types';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
import All from '~/assets/icons/restaurantCategory/all.svg';
import All from '~/assets/icons/category/all.svg';
import NavItem from './NavItem';

const meta: Meta<typeof NavItem> = {
Expand Down
42 changes: 1 addition & 41 deletions frontend/src/components/CelebDropDown/CelebDropDown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,4 @@ export default meta;

type Story = StoryObj<typeof CelebDropDown>;

export const Default: Story = {
args: {
celebs: [
{
id: 1,
name: '히밥',
youtubeChannelName: '@heebab',
profileImageUrl:
'https://yt3.googleusercontent.com/sL5ugPfl9vvwRwhf6l5APY__BZBw8qWiwgHs-uVsMPFoD5-a4opTJIcRSyrY8aY5LEESOMWJ=s176-c-k-c0x00ffffff-no-rj',
},
{
id: 2,
name: '정찬성',
youtubeChannelName: '@Korean_zzombi',
profileImageUrl:
'https://yt3.googleusercontent.com/sL5ugPfl9vvwRwhf6l5APY__BZBw8qWiwgHs-uVsMPFoD5-a4opTJIcRSyrY8aY5LEESOMWJ=s176-c-k-c0x00ffffff-no-rj',
},
{
id: 3,
name: '정찬',
youtubeChannelName: '@Korean_zzombi',
profileImageUrl:
'https://yt3.googleusercontent.com/sL5ugPfl9vvwRwhf6l5APY__BZBw8qWiwgHs-uVsMPFoD5-a4opTJIcRSyrY8aY5LEESOMWJ=s176-c-k-c0x00ffffff-no-rj',
},
{
id: 4,
name: '정성',
youtubeChannelName: '@Korean_zzombi',
profileImageUrl:
'https://yt3.googleusercontent.com/sL5ugPfl9vvwRwhf6l5APY__BZBw8qWiwgHs-uVsMPFoD5-a4opTJIcRSyrY8aY5LEESOMWJ=s176-c-k-c0x00ffffff-no-rj',
},
{
id: 5,
name: '정찬성1',
youtubeChannelName: '@Korean_zzombi',
profileImageUrl:
'https://yt3.googleusercontent.com/sL5ugPfl9vvwRwhf6l5APY__BZBw8qWiwgHs-uVsMPFoD5-a4opTJIcRSyrY8aY5LEESOMWJ=s176-c-k-c0x00ffffff-no-rj',
},
],
},
};
export const Default: Story = {};
96 changes: 31 additions & 65 deletions frontend/src/components/CelebDropDown/CelebDropDown.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,49 @@
import { styled, css } from 'styled-components';
import { MouseEvent, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { DropDown } from 'celuveat-ui-library';

import CelebIcon from '~/assets/icons/celeb.svg';
import ProfileImage from '~/components/@common/ProfileImage';
import NavItem from '~/components/@common/NavItem/NavItem';
import useBooleanState from '~/hooks/useBooleanState';

import type { Celeb } from '~/@types/celeb.types';
import { getCelebs } from '~/api/celeb';
import { BORDER_RADIUS, FONT_SIZE } from '~/styles/common';
import { OPTION_FOR_CELEB_ALL } from '~/constants/options';
import useMediaQuery from '~/hooks/useMediaQuery';

interface DropDownProps {
celebs: Celeb[];
isOpen?: boolean;
externalOnClick?: (e?: MouseEvent<HTMLLIElement>) => void;
}
import CelebNavItem from '~/components/CelebDropDown/CelebNavItem';
import CelebDropDownOption from '~/components/CelebDropDown/CelebDropDownOption';

function CelebDropDown({ celebs, externalOnClick, isOpen = false }: DropDownProps) {
import useMediaQuery from '~/hooks/useMediaQuery';
import { useCelebSelect } from '~/components/CelebDropDown/hooks/useCelebSelect';

function CelebDropDown() {
const { isMobile } = useMediaQuery();
const [selected, setSelected] = useState<Celeb>(OPTION_FOR_CELEB_ALL);
const { value: isShow, toggle: onToggleDropDown, setFalse: onCloseDropDown } = useBooleanState(isOpen);

const onSelection = (celebName: Celeb['name']) => (event?: MouseEvent<HTMLLIElement>) => {
setSelected(celebs.find(celeb => celebName === celeb.name));
const { selectedCeleb, selectCeleb } = useCelebSelect();

if (externalOnClick) externalOnClick(event);
};
const { data: celebOptions } = useQuery({
queryKey: ['celebOptions'],
queryFn: getCelebs,
suspense: true,
});

return (
<StyledCelebDropDown aria-hidden>
<StyledNavItemWrapper type="button" onClick={onToggleDropDown} onBlur={onCloseDropDown}>
{selected.id === -1 ? (
<NavItem label="전체 셀럽" icon={<CelebIcon />} isShow={isShow} />
) : (
<NavItem
label={selected.youtubeChannelName.replace('@', '')}
icon={<ProfileImage name={selected.name} imageUrl={selected.profileImageUrl} size="40px" />}
isShow={isShow}
/>
)}
</StyledNavItemWrapper>

{isShow && (
<DropDown>
<DropDown.Trigger isCustom>
<StyledNavItemWrapper>
<CelebNavItem celeb={selectedCeleb} />
</StyledNavItemWrapper>
</DropDown.Trigger>
<DropDown.Options as="ul" isCustom>
<StyledDropDownWrapper isMobile={isMobile}>
<StyledSelectContainer>
{celebs.map(({ id, name, youtubeChannelName, profileImageUrl }) => (
<StyledDropDownOption data-id={id} onMouseDown={onSelection(name)}>
<div>
{id === -1 ? <CelebIcon /> : <ProfileImage name={name} imageUrl={profileImageUrl} size="32px" />}
<div>
<StyledCelebName>{name}</StyledCelebName>
<StyledChannelName>{youtubeChannelName}</StyledChannelName>
</div>
</div>
</StyledDropDownOption>
{[OPTION_FOR_CELEB_ALL, ...celebOptions].map(celeb => (
<DropDown.Option as="li" key={celeb.id} externalClick={selectCeleb(celeb)} isCustom>
<StyledDropDownOption>
<CelebDropDownOption celeb={celeb} />
</StyledDropDownOption>
</DropDown.Option>
))}
</StyledSelectContainer>
</StyledDropDownWrapper>
)}
</StyledCelebDropDown>
</DropDown.Options>
</DropDown>
);
}

Expand All @@ -73,31 +57,24 @@ const StyledNavItemWrapper = styled.button`
outline: none;
`;

const StyledCelebDropDown = styled.div`
position: relative;
`;

const StyledDropDownWrapper = styled.ul<{ isMobile: boolean }>`
display: flex;
flex-direction: column;
align-content: center;

position: absolute;
top: calc(100% + 16px);
left: 0;
z-index: 1;

${({ isMobile }) =>
isMobile
? css`
left: 0;

width: 90vw;
height: 60vh;
max-height: 440px;
`
: css`
left: 18px;

width: 380px;
height: 440px;
`}
Expand Down Expand Up @@ -150,14 +127,3 @@ const StyledDropDownOption = styled.li`
background-color: var(--gray-1);
}
`;

const StyledCelebName = styled.div`
font-family: SUIT-Medium;
`;

const StyledChannelName = styled.div`
padding-top: 0.4rem;

color: var(--gray-3);
font-size: ${FONT_SIZE.sm};
`;
39 changes: 39 additions & 0 deletions frontend/src/components/CelebDropDown/CelebDropDownOption.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { styled } from 'styled-components';
import { Celeb } from '~/@types/celeb.types';
import ProfileImage from '~/components/@common/ProfileImage';
import { FONT_SIZE } from '~/styles/common';

import CelebIcon from '~/assets/icons/celeb.svg';

interface CelebDropDownOptionProps {
celeb: Celeb;
}

function CelebDropDownOption({ celeb }: CelebDropDownOptionProps) {
return (
<div>
{celeb.id === -1 ? (
<CelebIcon />
) : (
<ProfileImage name={celeb.name} imageUrl={celeb.profileImageUrl} size="32px" />
)}
<div>
<StyledCelebName>{celeb.name}</StyledCelebName>
<StyledChannelName>{celeb.youtubeChannelName}</StyledChannelName>
</div>
</div>
);
}

export default CelebDropDownOption;

const StyledCelebName = styled.div`
font-family: SUIT-Medium, sans-serif;
`;

const StyledChannelName = styled.div`
padding-top: 0.4rem;

color: var(--gray-3);
font-size: ${FONT_SIZE.sm};
`;
26 changes: 26 additions & 0 deletions frontend/src/components/CelebDropDown/CelebNavItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import NavItem from '~/components/@common/NavItem';
import ProfileImage from '~/components/@common/ProfileImage';
import CelebIcon from '~/assets/icons/celeb.svg';

import { Celeb } from '~/@types/celeb.types';

interface CelebNavItemProps {
celeb: Celeb;
}

function CelebNavItem({ celeb }: CelebNavItemProps) {
return (
<div>
{celeb.id === -1 ? (
<NavItem label="전체 셀럽" icon={<CelebIcon />} />
) : (
<NavItem
label={celeb.youtubeChannelName.replace('@', '')}
icon={<ProfileImage name={celeb.name} imageUrl={celeb.profileImageUrl} size="40px" />}
/>
)}
</div>
);
}

export default CelebNavItem;
27 changes: 27 additions & 0 deletions frontend/src/components/CelebDropDown/hooks/useCelebSelect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useState } from 'react';
import { shallow } from 'zustand/shallow';
import { OPTION_FOR_CELEB_ALL } from '~/constants/options';
import useRestaurantsQueryStringState from '~/hooks/store/useRestaurantsQueryStringState';

import { Celeb } from '~/@types/celeb.types';

export const useCelebSelect = () => {
const [selectedCeleb, setSelectedCeleb] = useState<Celeb>(OPTION_FOR_CELEB_ALL);

const [setCelebId, setCurrentPage] = useRestaurantsQueryStringState(
state => [state.setCelebId, state.setCurrentPage],
shallow,
);

const selectCeleb = (celeb: Celeb) => () => {
setCelebId(celeb.id);
setCurrentPage(0);

setSelectedCeleb(celeb);
};

return {
selectedCeleb,
selectCeleb,
};
};
3 changes: 3 additions & 0 deletions frontend/src/components/InfoDropDown/InfoDropDown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import InfoDropDown from './InfoDropDown';
const meta: Meta<typeof InfoDropDown> = {
title: 'Selector/InfoDropDown',
component: InfoDropDown,
parameters: {
layout: 'centered',
},
};

export default meta;
Expand Down
Loading
Loading