Skip to content

Commit

Permalink
Feat: 모바일 환경 반응형 헤더 구현 (#87)
Browse files Browse the repository at this point in the history
* Fix: 헤더 버튼 selectedDot 위치 문제 해결

* Feat: 헤더 반응형 로직 구현

* Feat: 검색창 컴포넌트 반응형 개선

* Feat: 모바일 환경 헤더 다크모드 버튼 추가

---------

Co-authored-by: Cho-Ik-Jun <[email protected]>
  • Loading branch information
Jaeppetto and harry7435 authored Jan 16, 2024
1 parent 4817094 commit 4a244eb
Show file tree
Hide file tree
Showing 13 changed files with 246 additions and 11 deletions.
2 changes: 2 additions & 0 deletions src/Components/Common/DropDownOnlyOption/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const DropDownOnlyOption = forwardRef(
labelProps,
isShow = true,
initialValue,
inset,
...props
}: DropDownProps,
ref: ForwardedRef<HTMLDivElement>,
Expand All @@ -51,6 +52,7 @@ const DropDownOnlyOption = forwardRef(
return (
<StyledDropDown
ref={ref}
$inset={inset}
{...props}
>
{label && (
Expand Down
3 changes: 2 additions & 1 deletion src/Components/Common/DropDownOnlyOption/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
StyledLabelProp,
} from './type';

export const StyledDropDown = styled.div`
export const StyledDropDown = styled.div<{ $inset?: string }>`
position: absolute;
inset: ${({ $inset }) => $inset};
`;

export const StyledLabel = styled.span<StyledLabelProp>`
Expand Down
1 change: 1 addition & 0 deletions src/Components/Common/DropDownOnlyOption/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface DropDownProps {
isShow?: boolean;
initialValue?: string;
onSelect?: (selected: string) => void;
inset?: string;

buttonProps?: HTMLAttributes<HTMLElement>;
optionProps?: HTMLAttributes<HTMLElement>;
Expand Down
167 changes: 167 additions & 0 deletions src/Components/Common/Header/Hamburger/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTheme } from 'styled-components';
import StyledWrapper from './style';

import DropDownOnlyOption from '../../DropDownOnlyOption';
import useTabStore from '@/Stores/Tab';
import useClickAway from '@/Hooks/UseClickAway';
import ModalButton from '../HeaderTab/ModalButton';
import { checkAuth, logout } from '@/Services/Auth';
import NotificationModal from '@/Components/NotificationModal';
import { useDarkModeStore } from '@/Stores';

const Hamburger = () => {
const location = useLocation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { colors } = useTheme();

const [isDrop, setIsDrop] = useState<boolean>(false);
const { tab, prev, setTab, setPrev } = useTabStore();
const [alarm, setAlarm] = useState(false);

const { isDarkMode, toggleDarkMode } = useDarkModeStore();
const isAuthUser = !!sessionStorage.getItem('AUTH_TOKEN');
const { data } = useQuery({
queryKey: ['auth'],
queryFn: checkAuth,
});

useEffect(() => {
if (
tab === 'home' ||
tab === 'message' ||
(tab === 'account' && location.pathname.includes('/profile'))
) {
setPrev(tab);
sessionStorage.setItem('prev', prev);
}
sessionStorage.setItem('tab', tab);
}, [tab, prev, setPrev, location]);

const styledNavIcon = {
fontSize: '3.2rem',
color: colors.text,
fontWeight: '300',
};

const { mutate: mutateLogout } = useMutation({
mutationFn: logout,
onSuccess: async () => {
// 로그아웃 성공 시
queryClient.setQueryData(['auth'], null);
sessionStorage.removeItem('AUTH_TOKEN');
navigate('/');
setTab('home');
},
});

const options: string[] = isAuthUser
? [
'홈',
'포스트 작성',
'검색',
'알림',
'메시지',
'마이페이지',
'비밀번호 변경',
'로그아웃',
isDarkMode ? '🌞 라이트모드' : '🌝 다크모드',
]
: ['홈', '포스트 작성', '검색', '로그인', '다크모드'];

const onSelectOption = (option: string) => {
setIsDrop(!isDrop);

switch (option) {
case '홈':
navigate('/');
break;
case '포스트 작성':
navigate(
`${location.pathname !== '/' ? location.pathname : ''}/add-post`,
);
break;
case '검색':
navigate(
`${location.pathname !== '/' ? location.pathname : ''}/search`,
);
break;
case '알림':
setAlarm((prevIsShow) => !prevIsShow);
break;
case '메시지':
navigate('/directmessage');
break;
case '로그인':
navigate('/login');
break;
case '마이페이지':
// eslint-disable-next-line no-underscore-dangle
navigate(`/profile/${data?._id}`);
setPrev('account');
break;
case '비밀번호 변경':
navigate(
`${
location.pathname !== '/' ? location.pathname : ''
// eslint-disable-next-line no-underscore-dangle
}/edit-password/${data?._id}`,
);
break;
case '로그아웃':
mutateLogout();
break;
case '🌞 라이트모드':
toggleDarkMode();
break;
case '🌝 다크모드':
toggleDarkMode();
break;
default:
break;
}
};

const ref = useClickAway((e) => {
setIsDrop(false);
setTab(prev);
});

return (
<>
{' '}
<StyledWrapper>
<ModalButton
name="menu"
style={styledNavIcon}
color={
tab === 'account' ? colors.primaryReverse : colors.primaryNormal
}
setModalOpen={() => setIsDrop(!isDrop)}
/>
<DropDownOnlyOption
ref={ref}
isShow={isDrop}
options={options}
onSelect={(option) => {
onSelectOption(option);
}}
inset="5rem 0rem 0rem -12rem"
/>
</StyledWrapper>
{alarm && (
<NotificationModal
onClose={() => {
setAlarm(false);
setTab(prev);
}}
/>
)}
</>
);
};

export default Hamburger;
10 changes: 10 additions & 0 deletions src/Components/Common/Header/Hamburger/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styled from 'styled-components';

const StyledWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
position: relative;
`;

export default StyledWrapper;
8 changes: 8 additions & 0 deletions src/Components/Common/Header/HeaderLogo/style.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { styled } from 'styled-components';
import { floatSmall } from '@/Styles/Animation';

export const StyledLogo = styled.img`
width: 20rem;
Expand All @@ -9,6 +10,13 @@ export const StyledLogo = styled.img`
&:hover {
transform: scale(1.1);
}
@media ${({ theme }) => theme.device.tablet} {
&:hover {
transform: none;
}
animation: ${floatSmall} 3s ease-in-out infinite;
}
`;

export const StyledContainer = styled.div`
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Common/Header/HeaderTab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ const HeaderTab = () => {
onSelect={(option) => {
onSelectOption(option);
}}
// style={{ right: '16rem', top: '5rem' }}
inset="5rem 0rem 0rem -11rem"
/>
<StyledFocusedCircle $visible={tab === 'account'} />
</StyledButtonContainer>
Expand Down
5 changes: 0 additions & 5 deletions src/Components/Common/Header/HeaderTab/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ export const StyledButtonContainer = styled.div`
transition: transform 0.3s ease-in-out;
}
}
> :nth-child(2) {
right: 16rem;
top: 5rem;
}
`;

export const StyledFocusedCircle = styled.div<{ $visible: boolean }>`
Expand Down
7 changes: 6 additions & 1 deletion src/Components/Common/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import { StyledHeaderContainer, StyledDivider } from './style';
import HeaderTab from './HeaderTab';
import HeaderLogo from './HeaderLogo';
import HeaderProps from './type';
import useResize from '@/Hooks/useResize';
import Hamburger from './Hamburger';
// import useTabStore from '@/Stores/Tab';

const Header = ({ activeHeader }: HeaderProps) => {
const { isMobileSize } = useResize();

// 뒤로가기 핸들러..
/**
const { prev, setTab } = useTabStore();
Expand All @@ -33,7 +37,8 @@ const Header = ({ activeHeader }: HeaderProps) => {
<StyledHeaderContainer>
<HeaderLogo />
<StyledDivider />
<HeaderTab />

{isMobileSize ? <Hamburger /> : <HeaderTab />}
</StyledHeaderContainer>
);
};
Expand Down
4 changes: 4 additions & 0 deletions src/Components/Common/Header/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export const StyledHeaderContainer = styled.div`
display: flex;
position: fixed;
z-index: 9;
@media ${({ theme }) => theme.device.tablet} {
padding: 2rem 2rem;
}
`;

export const StyledDivider = styled.div`
Expand Down
23 changes: 20 additions & 3 deletions src/Components/SearchModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ import SearchPostList from './SearchPostList';
import { Props } from './type';
import {
StyledBody,
StyledButton,
StyledHeader,
StyledHeaderTab,
StyledHeaderTitle,
StyledWrapper,
} from './style';
import useResize from '@/Hooks/useResize';
import Icon from '../Base/Icon';

// TODO: SearchPostList, SearchUserList 컴포넌트 통합
// TODO: 검색 결과 상세화
Expand All @@ -30,6 +33,8 @@ const SearchModal = ({ onChangeOpen }: Props) => {
const [userResult, setUserResult] = useState<UserType[] | null>(null);
const [postResult, setPostResult] = useState<PostType[] | null>(null);

const { isMobileSize } = useResize();

/**
* @brief 사용자가 입력한 검색어를 담아 요청을 보냅니다. 응답 데이터는 유저와 포스트 결과를 담고 있는 배열 형태이며, 이를 순회하며 유저 데이터와 포스트 데이터를 분리합니다. 이후 분리한 데이터를 하위 컴포넌트에게 전달하여 검색 결과를 화면에 표시합니다.
*/
Expand Down Expand Up @@ -81,12 +86,24 @@ const SearchModal = ({ onChangeOpen }: Props) => {
/>
)}
<Modal
width={40}
height={80}
width={isMobileSize ? 100 : 50}
height={isMobileSize ? 100 : 80}
onChangeOpen={onChangeOpen}
style={{ minWidth: '40rem' }}
style={{
minWidth: '40rem',
}}
>
<StyledWrapper>
{isMobileSize && (
<StyledButton
width="3rem"
height="3rem"
onClick={() => onChangeOpen(false)}
hoverBackgroundColor="tranparent"
>
<Icon name="close" />
</StyledButton>
)}
<StyledHeader>
<StyledHeaderTitle>
{searchQuery ? `"${searchQuery}" 검색 결과` : '검색'}
Expand Down
12 changes: 12 additions & 0 deletions src/Components/SearchModal/style.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import styled from 'styled-components';
import Button from '../Base/Button';

export const StyledWrapper = styled.div`
width: 100%;
Expand All @@ -7,11 +8,18 @@ export const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
position: relative;
justify-content: start;
align-items: center;
`;

export const StyledButton = styled(Button)`
position: absolute;
top: 2rem;
right: 3rem;
`;

export const StyledHeader = styled.header`
width: 100%;
height: auto;
Expand All @@ -23,6 +31,10 @@ export const StyledHeader = styled.header`
margin-bottom: 2rem;
cursor: default;
@media ${({ theme }) => theme.device.tablet} {
padding: 2rem;
}
`;

export const StyledHeaderTitle = styled.h1`
Expand Down
13 changes: 13 additions & 0 deletions src/Styles/Animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,16 @@ export const slideOut = keyframes`
opacity: 0;
}
`;

export const floatSmall = keyframes`
0% {
transform: translateY(0rem);
}
50% {
transform: translateY(-0.6rem);
}
100% {
transform: translateY(0rem);
}
`;

0 comments on commit 4a244eb

Please sign in to comment.