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

Header 알림 기능 구현 (issue#45) #58

Merged
merged 11 commits into from
Jul 19, 2024
Merged
4 changes: 3 additions & 1 deletion frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
node_modules
.env
.DS_Store
.DS_Store
dist
settings.json
16 changes: 8 additions & 8 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>데벨업 프론트엔드</title>
</head>
<body>
<div id="root"></div>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>데벨업 프론트엔드</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
9 changes: 9 additions & 0 deletions frontend/src/assets/images/bell.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions frontend/src/components/common/ListenKeyDown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect } from 'react';

interface ListenKeyDownProps {
targetKey: string;
onKeyDown: () => void;
}

export default function ListenKeyDown({ targetKey, onKeyDown }: ListenKeyDownProps) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === targetKey) {
onKeyDown();
}
};

window.addEventListener('keydown', handleKeyDown);

return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);

Check warning on line 21 in frontend/src/components/common/ListenKeyDown.tsx

View workflow job for this annotation

GitHub Actions / build-and-test

React Hook useEffect has missing dependencies: 'onKeyDown' and 'targetKey'. Either include them or remove the dependency array. If 'onKeyDown' changes too often, find the parent component that defines it and wrap that definition in useCallback

return null;
}
13 changes: 12 additions & 1 deletion frontend/src/components/header/Header.styled.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Bell from '@/assets/images/bell.svg';
import styled from 'styled-components';

export const Container = styled.nav`
Expand All @@ -13,8 +14,9 @@ export const Container = styled.nav`
0 4px 12px rgba(0, 0, 0, 0.08);

display: flex;
justify-content: space-between;
align-items: center;
padding-left: 20rem;
padding: 0 20rem;
`;

export const LogoImg = styled.img`
Expand All @@ -29,3 +31,12 @@ export const Logo = styled.h1`
export const Spacer = styled.div`
height: 6rem;
`;

export const BellIcon = styled(Bell)`
width: 2.8rem;
cursor: pointer;
`;

export const RightPart = styled.div``;

export const LeftPart = styled.div``;
26 changes: 23 additions & 3 deletions frontend/src/components/header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
import { Link } from 'react-router-dom';
import * as S from './Header.styled';
import { ROUTES } from '@/constants/routes';
import NotiModal from './notiModal';
import { useState } from 'react';

export default function Header() {
const [isModalOpen, setIsModalOpen] = useState(false);

const handleBellClick = () => {
setIsModalOpen((prev) => !prev);
};

const closeModal = () => {
if (isModalOpen) {
setIsModalOpen(false);
}
};

return (
<>
<S.Container>
<Link to={ROUTES.main}>
<S.Logo>🚀 Devel Up</S.Logo>
</Link>
<S.LeftPart>
<Link to={ROUTES.main}>
<S.Logo>🚀 Devel Up</S.Logo>
</Link>
</S.LeftPart>
<S.RightPart>
<S.BellIcon onClick={handleBellClick} />
</S.RightPart>
</S.Container>
{isModalOpen && <NotiModal closeModal={closeModal} />}
<S.Spacer />
</>
);
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/components/header/notiModal/NotiList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as S from './NotiModal.styled';
import { NOTI_MOCKS } from './notiMocks';

export default function NotiList() {
const handleRead = () => {
alert('읽음');
};

// 알림 data를 일단 mock으로 처리 @라이언
const notifications = NOTI_MOCKS;

const isEmpty = notifications.length === 0;

return isEmpty ? (
<div>알림이 없습니다.</div>
) : (
notifications.map(({ id, message }) => (
<S.NotiItem key={id}>
{message}
<S.NotiReadBtn onClick={handleRead}>✅</S.NotiReadBtn>
</S.NotiItem>
))
);
}
39 changes: 39 additions & 0 deletions frontend/src/components/header/notiModal/NotiModal.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import styled from 'styled-components';

export const NotiModalContainer = styled.div`
z-index: 101;
position: fixed;
top: 5.5rem;
right: 17rem;
width: 30rem;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);

border-radius: 1rem;
padding: 2rem;
background-color: var(--grey-100);
`;

export const NotiItem = styled.div`
position: relative;
font-size: 1.1rem;
margin-bottom: 1rem;
padding: 1rem;
border-radius: 1rem;
background-color: white;
box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1);
`;

export const NotiTitle = styled.h2`
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 1.5rem;
`;

export const NotiReadBtn = styled.button`
font-size: 1.5rem;
width: 1.5rem;
height: 1.5rem;
position: absolute;
right: 0;
bottom: 0;
`;
20 changes: 20 additions & 0 deletions frontend/src/components/header/notiModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import NotiList from './NotiList';
import * as S from './NotiModal.styled';
import ListenKeyDown from '@/components/common/ListenKeyDown';
import useClickOutside from '@/hooks/useClickOutside';

interface NotiModalProps {
closeModal: () => void;
}

export default function NotiModal({ closeModal }: NotiModalProps) {
const { targetRef } = useClickOutside<HTMLDivElement>(closeModal);

return (
<S.NotiModalContainer ref={targetRef}>
<ListenKeyDown targetKey="Escape" onKeyDown={closeModal} />
<S.NotiTitle>🔔 알림</S.NotiTitle>
<NotiList />
</S.NotiModalContainer>
);
}
18 changes: 18 additions & 0 deletions frontend/src/components/header/notiModal/notiMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface Notification {
id: number;
message: string;
isRead: boolean;
}

export const NOTI_MOCKS: Notification[] = [
{
id: 1,
message: '버건디님과 페어 매칭이 완료되었습니다.',
isRead: false,
},
{
id: 2,
message: '리브님으로부터 리뷰가 도착했습니다. 지금 미션 현황 페이지에서 확인해보세요.',
isRead: false,
},
];
31 changes: 31 additions & 0 deletions frontend/src/hooks/useClickOutside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { router } from '@/index';
import { useEffect, useRef } from 'react';

const useClickOutside = <T extends Node = HTMLElement>(callback: () => void) => {
const ref = useRef<T>(null);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
callback();
}
};

// 이벤트 버블링에 의해 모달이 렌더되는 즉시 callback이 호출되는 것을 방지하기 위해 setTimeout 사용 (추후 리팩토링 예정) @라이언
const timer = setTimeout(() => {
document.addEventListener('click', handleClickOutside);
}, 0);

return () => {
clearTimeout(timer);
document.removeEventListener('click', handleClickOutside);
};
}, [callback]);

// react-router-dom Link 클릭 시 클릭 이벤트가 감지되지 않는 문제를 해결하기 위함 @라이언
router.subscribe(callback);

return { targetRef: ref };
};

export default useClickOutside;
2 changes: 1 addition & 1 deletion frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const routes = [
},
];

const router = createBrowserRouter(routes, {
export const router = createBrowserRouter(routes, {
Copy link
Contributor

Choose a reason for hiding this comment

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

이놈의 라우터..

basename: ROUTES.main,
});

Expand Down
9 changes: 9 additions & 0 deletions frontend/src/styles/GlobalStyle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ html,
text-decoration: none;
color:black;
}

/* Hide scrollbar */
::-webkit-scrollbar {
display: none; /* webkit browsers */
}
* {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
}
#root {
--primary-50:#E7E9F8;
--primary-100:#C4C9ED;
Expand Down
Loading