Skip to content

Commit

Permalink
[Design] Mobile ver. - Header 반응형 구현 (#371)
Browse files Browse the repository at this point in the history
* init: 파일 생성

* fix: theme color 네이밍 범용적으로 수정

* move: Nav 디렉토리 만들어서 컴포넌트 분리

* feat: react-responsive 설치 및 useDevice hook 추가

* feat: isTablet, isMobile 공용 useDevice 추가

* fix: Logo svg className prop 확장

* chore: PC지원 막기 임시 해제

* design: tab/mob ver. 헤더 로고, 햄버거 조건부 렌더링

* design: 햄버거 토클 시 background 변경

* fix: TextBox이메일 width 처리 누락 해결

* fix: 달라진 useDevice 반환값에 따른 수정

* fix: header position fixed로 수정 및 layout marginTop 추가

* design: header padding 반응형

* fix: 디자인 변경사항 (padding값) 반영

* feat: Nav 내부의 MenuList 컴포넌트 분리

* design: 드롬메뉴 헤더 반응형

* feat: DEVICE_TYPE에 따라 isMenuOpen값 초기화

* feat: 햄버거 버튼과 닫기 버튼

* design: dimmed 배경

* feat: dimmed 배경 클릭 시 헤더 닫히도록

* design: 반응형 header 드롭메뉴 애니메이션

* design: 반응형 header active 페이지 bolder 추가

* design: 햄버거버튼 다크모드 조건부스타일링

* feat: 애니메이션 keyframes 공통 animationa.css.ts로 분리
  • Loading branch information
lydiacho authored Aug 11, 2024
1 parent 97ec62c commit c26e2cd
Show file tree
Hide file tree
Showing 19 changed files with 433 additions and 102 deletions.
10 changes: 8 additions & 2 deletions src/common/assets/MakersDarkLogo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
const MakersDarkLogo = () => {
const MakersDarkLogo = ({ className }: { className?: string }) => {
return (
<svg width="87" height="31" viewBox="0 0 87 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="87"
height="31"
viewBox="0 0 87 31"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<g clipPath="url(#clip0_2501_966)">
<path
fillRule="evenodd"
Expand Down
10 changes: 8 additions & 2 deletions src/common/assets/MakersLogo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
const MakersLogo = () => {
const MakersLogo = ({ className }: { className?: string }) => {
return (
<svg width="87" height="30" viewBox="0 0 87 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="87"
height="30"
viewBox="0 0 87 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<g clipPath="url(#clip0_2311_4700)">
<path
fillRule="evenodd"
Expand Down
10 changes: 8 additions & 2 deletions src/common/assets/NowsoptLogo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
const NowsoptLogo = () => {
const NowsoptLogo = ({ className }: { className?: string }) => {
return (
<svg width="87" height="30" viewBox="0 0 87 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="87"
height="30"
viewBox="0 0 87 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<g id="Frame" clipPath="url(#clip0_1535_12708)">
<g id="Group">
<path
Expand Down
7 changes: 5 additions & 2 deletions src/common/components/Input/components/InputTheme/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ export const TextBox이메일 = ({
return (
<TextBox label="이메일" name="email" required>
<InputLine
style={{ width: 308, paddingRight: isActive ? 50 : 16 }}
style={{
width: DEVICE_TYPE === 'DESK' ? 308 : DEVICE_TYPE === 'TAB' ? 246 : 208,
paddingRight: isActive ? 50 : 16,
}}
name="email"
placeholder="이메일을 입력해주세요."
type="email"
Expand All @@ -158,7 +161,7 @@ export const TextBox이메일 = ({
<Timer isActive={isActive} onResetTimer={handleResetTimer} />
</InputLine>
<InputLine
style={{ width: 308 }}
style={{ width: DEVICE_TYPE === 'DESK' ? 308 : DEVICE_TYPE === 'TAB' ? 246 : 208 }}
id="verification-code"
readOnly={!isActive}
name="code"
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { track } from '@amplitude/analytics-browser';
import { NavLink } from 'react-router-dom';

import { menuItem, menuLink } from './style.css';
import { useDevice } from '@hooks/useDevice';

import { menuItemVar, menuLinkVar } from './style.css';

interface MenuItemProps {
text: string;
Expand All @@ -13,18 +15,20 @@ interface MenuItemProps {
}

const MenuItem = ({ text, path, target, amplitudeId, className, onClick }: MenuItemProps) => {
const DEVICE_TYPE = useDevice();

return (
<li className={`${className} ${menuItem}`}>
<li className={`${className} ${menuItemVar[DEVICE_TYPE]}`}>
{path ? (
<NavLink
to={path}
className={menuLink}
className={menuLinkVar[DEVICE_TYPE]}
onClick={() => (amplitudeId ? track(amplitudeId) : null)}
target={target}>
{text}
</NavLink>
) : (
<p className={`${onClick ? menuLink : null}`} onClick={onClick}>
<p className={`${onClick ? menuLinkVar[DEVICE_TYPE] : null}`} onClick={onClick}>
{text}
</p>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { style, styleVariants } from '@vanilla-extract/css';

import { theme } from 'styles/theme.css';

const menuItem = style({
...theme.font.HEADING_6_18_B,
});

export const menuItemVar = styleVariants({
DESK: [
menuItem,
{
color: theme.color.baseText,
},
],
TAB: [
menuItem,
{
color: theme.color.grayButtonFill,
},
],
MOB: [
menuItem,
{
color: theme.color.grayButtonFill,
},
],
});

const menuLink = style({
textDecoration: `underline transparent 2px`,
textUnderlineOffset: 21,
transition: 'all 0.2s ease-out',
cursor: 'pointer',
});

export const menuLinkVar = styleVariants(
{
DESK: {
hover: {
textDecorationColor: theme.color.primary,
},
active: {
color: theme.color.primary,
},
},
TAB: {
hover: {
color: theme.color.whiteButtonFill,
},
active: {
color: theme.color.whiteButtonFill,
fontWeight: 'bolder',
},
},
MOB: {
hover: {
color: theme.color.whiteButtonFill,
},
active: {
color: theme.color.whiteButtonFill,
fontWeight: 'bolder',
},
},
},
({ hover, active }) => [
menuLink,
{
selectors: {
'&:hover:not([disabled])': hover,
'&.active': active,
},
},
],
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { reset, track } from '@amplitude/analytics-browser';
import { useContext, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

import { useDevice } from '@hooks/useDevice';
import { RecruitingInfoContext } from '@store/recruitingInfoContext';

import { dimmedBgVar, menuContainerVar, menuList, menuMobListVar } from './style.css';
import { MENU_ITEMS, MENU_ITEMS_MAKERS } from '../../contants';
import MenuItem from '../MenuItem';

const MenuList = ({ isMenuOpen, onClickMenuToggle }: { isMenuOpen?: boolean; onClickMenuToggle?: () => void }) => {
const DEVICE_TYPE = useDevice();
const navigate = useNavigate();
const { pathname } = useLocation();
const [isShown, setIsShown] = useState(isMenuOpen);
const [animation, setAnimation] = useState<'open' | 'close'>(isMenuOpen ? 'open' : 'close');

useEffect(() => {
if (isMenuOpen) {
setIsShown(true);
setAnimation('open');
} else {
setAnimation('close');
const timer = setTimeout(() => setIsShown(false), 300);
return () => clearTimeout(timer);
}
}, [isMenuOpen]);

const isSignedIn = localStorage.getItem('soptApplyAccessToken');

const {
recruitingInfo: { name, isMakers },
} = useContext(RecruitingInfoContext);
const menuItems = isMakers ? MENU_ITEMS_MAKERS : MENU_ITEMS;
const handleLogout = () => {
track('click-gnb-logout');
reset();
localStorage.removeItem('soptApplyAccessToken');
localStorage.removeItem('soptApplyAccessTokenExpiredTime');
pathname === '/' ? window.location.reload() : navigate('/');
};

if (onClickMenuToggle && !isShown) return null;

return (
<nav>
<ul
className={DEVICE_TYPE !== 'DESK' ? `${menuMobListVar[DEVICE_TYPE]} ${menuContainerVar[animation]}` : menuList}>
{!isSignedIn && (
<>
{menuItems.map(({ text, path, target, amplitudeId }) => (
<MenuItem key={text} text={text} path={path} target={target} amplitudeId={amplitudeId} />
))}
<MenuItem key="로그인" text="로그인" path="/" amplitudeId="click-gnb-signin" />
</>
)}
{isSignedIn && name && (
<>
{menuItems.map(({ text, path, target, amplitudeId }) => (
<MenuItem key={text} text={text} path={path} target={target} amplitudeId={amplitudeId} />
))}
<MenuItem key="로그아웃" text="로그아웃" onClick={handleLogout} />
<MenuItem key="로그인완료" text={`${name}님`} className="amp-block" />
</>
)}
</ul>
{onClickMenuToggle && isShown && <div className={dimmedBgVar[animation]} onClick={onClickMenuToggle} />}
</nav>
);
};

export default MenuList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { style, styleVariants } from '@vanilla-extract/css';

import { Z_INDEX } from '@constants/zIndex';
import { fadeIn, fadeInDown, fadeOut, fadeOutUp } from 'styles/animation.css';
import { theme } from 'styles/theme.css';

export const menuContainerVar = styleVariants({
open: {
animation: `${fadeInDown} 0.3s`,
},
close: {
animation: `${fadeOutUp} 0.3s`,
animationFillMode: 'forwards',
},
});
export const menuList = style({
display: 'flex',
alignItems: 'center',
gap: 40,
});

const menuMobList = style({
display: 'flex',
flexDirection: 'column',
gap: 18,
position: 'fixed',
top: 0,
left: 0,
width: '100%',
backgroundColor: theme.color.blackBackground,
zIndex: Z_INDEX.gnbMenu,
});

export const menuMobListVar = styleVariants({
TAB: [
menuMobList,
{
padding: '92px 40px 40px 40px',
},
],
MOB: [
menuMobList,
{
padding: '86px 20px 36px 20px',
},
],
});

export const dimmedBg = style({
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100dvh',
backgroundColor: theme.color.backgroundDimmed,
zIndex: Z_INDEX.gnbBg,

cursor: 'pointer',
});

export const dimmedBgVar = styleVariants({
open: [
dimmedBg,
{
animation: `${fadeIn} 0.3s`,
},
],
close: [
dimmedBg,
{
animation: `${fadeOut} 0.3s`,
animationFillMode: 'forwards',
},
],
});
24 changes: 24 additions & 0 deletions src/common/components/Layout/components/Header/Nav/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { IconMenu, IconXClose } from '@sopt-makers/icons';
import { useContext } from 'react';

import { useDevice } from '@hooks/useDevice';
import { ThemeContext } from '@store/themeContext';
import { theme } from 'styles/theme.css';

import MenuList from './MenuList';
import { menuIconVar } from './style.css';

const Nav = ({ isMenuOpen, onClickMenuToggle }: { isMenuOpen: boolean; onClickMenuToggle: () => void }) => {
const DEVICE_TYPE = useDevice();
const { isLight } = useContext(ThemeContext);

return DEVICE_TYPE !== 'DESK' ? (
<i className={menuIconVar[DEVICE_TYPE]} onClick={onClickMenuToggle}>
{isMenuOpen ? <IconXClose /> : <IconMenu color={isLight ? 'currentColor' : theme.color.white} />}
</i>
) : (
<MenuList />
);
};

export default Nav;
Loading

0 comments on commit c26e2cd

Please sign in to comment.