Skip to content

Commit d3d05c6

Browse files
authored
Merge pull request #493 from velopert/feature/notifications
Feature/notifications
2 parents f3db873 + fd09833 commit d3d05c6

File tree

10 files changed

+137
-51
lines changed

10 files changed

+137
-51
lines changed

src/components/base/Header.tsx

Lines changed: 100 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useRef, useCallback } from 'react';
2-
import styled from 'styled-components';
3-
import { SearchIcon2 } from '../../static/svg';
2+
import styled, { css } from 'styled-components';
3+
import { NotificationIcon, SearchIcon3 } from '../../static/svg';
44
import RoundButton from '../common/RoundButton';
55
import MainResponsive from '../main/MainResponsive';
66
import useHeader from './hooks/useHeader';
@@ -11,19 +11,21 @@ import { Link } from 'react-router-dom';
1111
import media from '../../lib/styles/media';
1212
import HeaderLogo from './HeaderLogo';
1313
import { themedPalette } from '../../lib/styles/themes';
14-
import ThemeToggleButton from './ThemeToggleButton';
15-
import { useSelector } from 'react-redux';
16-
import { RootState } from '../../modules';
14+
import VLink from '../common/VLink';
15+
import { useDispatch } from 'react-redux';
16+
import { showAuthModal } from '../../modules/core';
17+
import { useQuery } from '@apollo/react-hooks';
18+
import { NOTIFICATION_COUNT } from '../../lib/graphql/notification';
1719

1820
export type MainHeaderProps = {};
1921

2022
function Header(props: MainHeaderProps) {
23+
const dispatch = useDispatch();
24+
const { data: notificationCountData } = useQuery(NOTIFICATION_COUNT);
25+
2126
const { user, onLoginClick, onLogout, customHeader } = useHeader();
2227
const [userMenu, toggleUserMenu] = useToggle(false);
2328
const ref = useRef<HTMLDivElement>(null);
24-
const themeReady = useSelector(
25-
(state: RootState) => state.darkMode.systemTheme !== 'not-ready',
26-
);
2729

2830
const onOutsideClick = useCallback(
2931
(e: React.MouseEvent) => {
@@ -34,6 +36,15 @@ function Header(props: MainHeaderProps) {
3436
[toggleUserMenu],
3537
);
3638

39+
const onClickNotification = (event: React.MouseEvent<HTMLAnchorElement>) => {
40+
if (!user) {
41+
event.preventDefault();
42+
dispatch(showAuthModal('LOGIN'));
43+
return;
44+
}
45+
};
46+
47+
const notificationCount = notificationCountData?.notificationCount ?? 0;
3748
const urlForSearch = customHeader.custom
3849
? `/search?username=${customHeader.username}`
3950
: '/search';
@@ -46,44 +57,48 @@ function Header(props: MainHeaderProps) {
4657
userLogo={customHeader.userLogo}
4758
username={customHeader.username}
4859
/>
60+
<Right>
61+
<NotificationButton to="/notifications" onClick={onClickNotification}>
62+
{user && notificationCount !== 0 && (
63+
<NotificationCounter
64+
isSingle={Math.floor(notificationCount / 10) === 0}
65+
>
66+
{Math.min(99, notificationCount)}
67+
</NotificationCounter>
68+
)}
69+
<NotificationIcon />
70+
</NotificationButton>
71+
<SearchButton to={urlForSearch}>
72+
<SearchIcon3 />
73+
</SearchButton>
74+
{user ? (
75+
<>
76+
<RoundButton
77+
border
78+
color="darkGray"
79+
style={{ marginRight: '1.25rem' }}
80+
to="/write"
81+
className="write-button"
82+
>
83+
새 글 작성
84+
</RoundButton>
4985

50-
{user ? (
51-
<Right>
52-
{themeReady && <ThemeToggleButton />}
53-
<SearchButton to={urlForSearch}>
54-
<SearchIcon2 />
55-
</SearchButton>
56-
<RoundButton
57-
border
58-
color="darkGray"
59-
style={{ marginRight: '1.25rem' }}
60-
to="/write"
61-
className="write-button"
62-
>
63-
새 글 작성
64-
</RoundButton>
65-
66-
<div ref={ref}>
67-
<HeaderUserIcon user={user} onClick={toggleUserMenu} />
68-
</div>
69-
<HeaderUserMenu
70-
onClose={onOutsideClick}
71-
onLogout={onLogout}
72-
username={user.username}
73-
visible={userMenu}
74-
/>
75-
</Right>
76-
) : (
77-
<Right>
78-
{themeReady && <ThemeToggleButton />}
79-
<SearchButton to={urlForSearch}>
80-
<SearchIcon2 />
81-
</SearchButton>
86+
<div ref={ref}>
87+
<HeaderUserIcon user={user} onClick={toggleUserMenu} />
88+
</div>
89+
<HeaderUserMenu
90+
onClose={onOutsideClick}
91+
onLogout={onLogout}
92+
username={user.username}
93+
visible={userMenu}
94+
/>
95+
</>
96+
) : (
8297
<RoundButton color="darkGray" onClick={onLoginClick}>
8398
로그인
8499
</RoundButton>
85-
</Right>
86-
)}
100+
)}
101+
</Right>
87102
</Inner>
88103
</Block>
89104
);
@@ -114,12 +129,53 @@ const SearchButton = styled(Link)`
114129
background: ${themedPalette.slight_layer};
115130
}
116131
svg {
117-
width: 1.125rem;
118-
height: 1.125rem;
132+
width: 24px;
133+
height: 24px;
119134
}
120135
margin-right: 0.5rem;
121136
`;
122137

138+
const NotificationButton = styled(VLink)`
139+
position: relative;
140+
display: flex;
141+
align-items: center;
142+
justify-content: center;
143+
background: transparent;
144+
border: none;
145+
width: 2.5rem;
146+
height: 2.5rem;
147+
outline: none;
148+
border-radius: 50%;
149+
color: ${themedPalette.text1};
150+
cursor: pointer;
151+
margin-right: 4px;
152+
&:hover {
153+
background: ${themedPalette.slight_layer};
154+
}
155+
svg {
156+
width: 24px;
157+
height: 24px;
158+
}
159+
`;
160+
161+
const NotificationCounter = styled.div<{ isSingle: boolean }>`
162+
position: absolute;
163+
top: 3px;
164+
right: -3px;
165+
padding: 1px 4px;
166+
font-weight: 500;
167+
font-size: 11px;
168+
background-color: var(--primary1);
169+
color: var(--button-text);
170+
border-radius: 100px;
171+
172+
${(props) =>
173+
props.isSingle &&
174+
css`
175+
right: 4px;
176+
`}
177+
`;
178+
123179
const Inner = styled(MainResponsive)`
124180
height: 100%;
125181
display: flex;

src/components/base/HeaderUserMenu.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ const HeaderUserMenu: React.FC<HeaderUserMenuProps> = ({
5252
</div>
5353
<HeaderUserMenuItem to="/saves">임시 글</HeaderUserMenuItem>
5454
<HeaderUserMenuItem to="/lists/liked">읽기 목록</HeaderUserMenuItem>
55-
<HeaderUserMenuItem to="/setting">설정</HeaderUserMenuItem>
55+
<HeaderUserMenuItem to="/setting" isMigrated={true}>
56+
설정
57+
</HeaderUserMenuItem>
5658
<HeaderUserMenuItem onClick={onLogout}>로그아웃</HeaderUserMenuItem>
5759
</div>
5860
</HeaderUserMenuBlock>

src/components/common/VLink.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,24 @@ type Props = {
66
className?: string;
77
children?: React.ReactNode;
88
style?: React.CSSProperties;
9+
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
910
};
1011

11-
function VLink({ to, children, className = '', style }: Props) {
12+
function VLink({ to, children, className = '', style, onClick }: Props) {
1213
const url = `${process.env.REACT_APP_CLIENT_V3_HOST}${to}`;
14+
15+
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
16+
if (!onClick) return;
17+
onClick(event);
18+
};
19+
1320
return (
14-
<Link href={url} className={className} style={style}>
21+
<Link
22+
href={url}
23+
className={className}
24+
style={style}
25+
onClick={(event) => handleClick(event)}
26+
>
1527
{children}
1628
</Link>
1729
);

src/components/post/PostCustomBanner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const onClick = () => {
1616
const PostCustomBanner: React.FC<PostCustomBannerProps> = ({ image, url }) => {
1717
return (
1818
<PostCustomBannerBlock onClick={onClick}>
19-
<a href={url} target="_blank">
19+
<a href={url} target="_blank" rel="noopener noreferrer">
2020
<img src={image} alt="post-custom-banner" />
2121
</a>
2222
</PostCustomBannerBlock>

src/containers/post/PostRepliesContainer.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ const PostRepliesContainer: React.FC<PostRepliesProps> = ({
6969
[onToggleAskRemove],
7070
);
7171

72-
console.log(replies);
7372
if (replies.loading || !replies.data) {
7473
return null;
7574
}

src/containers/post/PostViewer.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import MobileLikeButton from '../../components/post/MobileLikeButton';
4242
import RelatedPost from './RelatedPost';
4343
import optimizeImage from '../../lib/optimizeImage';
4444
import { useSetShowFooter } from '../../components/velog/VelogPageTemplate';
45-
import HorizontalBanner from './HorizontalBanner';
4645
import gtag from '../../lib/gtag';
4746
import FollowButton from '../../components/common/FollowButton';
4847
import { BANNER_ADS } from '../../lib/graphql/ad';

src/lib/graphql/notification.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { gql } from 'apollo-boost';
2+
3+
export const NOTIFICATION_COUNT = gql`
4+
query NotificationCount {
5+
notificationCount
6+
}
7+
`;

src/static/svg/icon-notification.svg

Lines changed: 5 additions & 0 deletions
Loading

src/static/svg/icon-search-3.svg

Lines changed: 5 additions & 0 deletions
Loading

src/static/svg/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ export { ReactComponent as TwitterIcon } from './icon-twitter.svg';
1818
export { ReactComponent as ShareIcon2 } from './icon-share-2.svg';
1919
export { ReactComponent as SearchIcon } from './icon-search.svg'; // iconmonstr-magnifier-3
2020
export { ReactComponent as SearchIcon2 } from './icon-search-2.svg';
21+
export { ReactComponent as SearchIcon3 } from './icon-search-3.svg';
2122
export { ReactComponent as VelogIcon } from './velog-icon.svg';
2223
export { ReactComponent as CheckIcon } from './icon-check.svg';
23-
24+
export { ReactComponent as NotificationIcon } from './icon-notification.svg';
2425
export { ReactComponent as MoonIcon } from './icon-moon.svg';
2526
export { ReactComponent as SunIcon } from './icon-sun.svg';

0 commit comments

Comments
 (0)