Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit 035ca7b
Author: Jeon Jinho <[email protected]>
Date:   Sun Mar 10 14:43:31 2024 +0900

    알림 기능 구현 (#245)

    * feat: add getNotification api

    * feat: add useUnreadNotifications

    * feat: add dot to unread item

    * feat: add read notification

commit 8fdd711
Author: Jeon Jinho <[email protected]>
Date:   Sun Mar 10 14:43:16 2024 +0900

    feat: add topic card action sheet (#244)

    * feat: add topic card action sheet

    * feat: disable 투표 다시하기

commit c9a80ca
Author: Jeon Jinho <[email protected]>
Date:   Thu Mar 7 22:12:17 2024 +0900

    캐시 무효화(cache busting) (#243)
  • Loading branch information
Jinho1011 committed Mar 10, 2024
1 parent f8ce293 commit 2c5ff25
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 80 deletions.
3 changes: 3 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<meta name="description" content="My Awesome App description" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>AB - 세상의 모든 질문, AB로 답하다</title>
<style>
@font-face {
Expand Down
50 changes: 50 additions & 0 deletions src/apis/notification/useNotifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';

import { NotificationResponse } from '@interfaces/api/notification';

import client from '@apis/fetch';

export const NOTICE_KEY = 'notifications';

const getNotifications = () => {
return client.get<NotificationResponse[]>('/notifications');
};

const getUnreadNotifications = () => {
return client.get<number>('/notifications/counts/unread ');
};

const readNotification = (id: number) => {
return client.post({ path: `/notifications/${id}/read`, body: {} });
};

const useNotifications = () => {
return useQuery({
queryKey: [NOTICE_KEY],
queryFn: getNotifications,
refetchOnMount: true,
});
};

const useUnreadNotifications = () => {
return useQuery({
queryKey: [NOTICE_KEY, 'unread'],
queryFn: getUnreadNotifications,
refetchOnMount: true,
});
};

const useReadNotification = (notificationId: number) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: () => readNotification(notificationId),
onMutate: () => {
queryClient.setQueryData([NOTICE_KEY], (prev: NotificationResponse[]) =>
prev.map((item) => (item.id === notificationId ? { ...item, isRead: true } : item))
);
},
});
};

export { useNotifications, useUnreadNotifications, useReadNotification };
58 changes: 56 additions & 2 deletions src/components/A/ATopicCard.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import React from 'react';

import useHideTopic from '@apis/topic/useHideTopic';
import useReportTopic from '@apis/topic/useReportTopic';
import Chip from '@components/commons/Chip/Chip';
import CommentChip from '@components/commons/Chip/CommentChip';
import { Col, Row } from '@components/commons/Flex/Flex';
import ProgressBar from '@components/commons/ProgressBar/ProgressBar';
import Text from '@components/commons/Text/Text';
import { Toast } from '@components/commons/Toast/Toast';
import TopicComments from '@components/Home/TopicComments/TopicComments';
import useBottomSheet from '@hooks/useBottomSheet/useBottomSheet';
import useActionSheet from '@hooks/useModal/useActionSheet';
import { TopicResponse } from '@interfaces/api/topic';

import { colors } from '@styles/theme';

import { HotIcon, MeatballIcon, TrendingIcon } from '@icons/index';
import {
HideIcon,
HotIcon,
MeatballIcon,
RefreshIcon,
ReportIcon,
TrashCanIcon,
TrendingIcon,
} from '@icons/index';

import { getDateDiff } from '@utils/date';

Expand All @@ -26,6 +38,8 @@ interface AlphaTopicCardProps {

const AlphaTopicCard = React.memo(({ topic, onVote, isTrending, isMine }: AlphaTopicCardProps) => {
const { BottomSheet: CommentSheet, toggleSheet } = useBottomSheet({});
const reportMutation = useReportTopic(topic.topicId);
const hideMutation = useHideTopic(topic.topicId);

const isRevealed = topic.selectedOption !== null || isMine;

Expand All @@ -46,14 +60,53 @@ const AlphaTopicCard = React.memo(({ topic, onVote, isTrending, isMine }: AlphaT
}
};

const handleOptionClick = () => {};
const handleOptionClick = () => {
toggleModal();
};

const TrendingChip = () => (
<Chip icon={<TrendingIcon />} tintColor={'#8CFF8A'} label={'실시간 인기 토픽'} />
);
const HotChip = () => <Chip icon={<HotIcon />} tintColor={'#FF61B7'} label={'치열한 경쟁 중'} />;
const TopicCardChip = () => (isTrending ? <TrendingChip /> : isHot ? <HotChip /> : null);

const handleHideTopic = () => {
hideMutation.mutate();
toggleModal();
Toast.error('관련 카테고리의 토픽을 더이상 추천하지 않아요.');
};

const handleReportTopic = () => {
reportMutation.mutate();
toggleModal();
Toast.error('해당 토픽을 신고하였어요.');
};

const handleRevoteTopic = () => {
throw new Error('투표 다시하기 기능을 사용할 수 없습니다.');
};

const { Modal: TopicModal, toggleModal } = useActionSheet({
actions: [
{
icon: <HideIcon />,
label: '이런 토픽은 안볼래요',
onClick: handleHideTopic,
},
{
icon: <ReportIcon />,
label: '신고하기',
onClick: handleReportTopic,
},
{
icon: <RefreshIcon fill={topic.selectedOption === null ? colors.black_20 : colors.black} />,
label: '투표 다시 하기',
onClick: handleRevoteTopic,
disabled: true,
},
],
});

return (
<>
<Container>
Expand Down Expand Up @@ -119,6 +172,7 @@ const AlphaTopicCard = React.memo(({ topic, onVote, isTrending, isMine }: AlphaT
<CommentSheet>
<TopicComments topic={topic} />
</CommentSheet>
<TopicModal />
</>
);
});
Expand Down
54 changes: 37 additions & 17 deletions src/components/Notifications/NotificationItem.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import React from 'react';

import { useReadNotification } from '@apis/notification/useNotifications';
import { Col, Row } from '@components/commons/Flex/Flex';
import Text from '@components/commons/Text/Text';
import { NotificationResponse } from '@interfaces/api/notification';

import { colors } from '@styles/theme';

import { ClockIcon, CommentIcon, HitIcon, ThumbsIcon } from '@icons/index';

import { getDateDiff } from '@utils/date';

import { IconWrapper } from './NotificationItem.styles';

interface NotificationItem {
onClick: () => void;
notification: {
type: 'hit' | 'comment' | 'like' | 'close';
title: string;
date: number;
checked: boolean;
};
notification: NotificationResponse;
}

const NotificationItem = ({ onClick, notification }: NotificationItem) => {
const NotificationItem = ({ notification }: NotificationItem) => {
const readNotification = useReadNotification(notification.id);

const renderIcon = () => {
switch (notification.type) {
case 'close':
case 'VOTE_RESULT':
return <ClockIcon />;
case 'comment':
case 'COMMENT_ON_TOPIC':
return <CommentIcon />;
case 'hit':
case 'VOTE_COUNT_ON_TOPIC':
return <HitIcon />;
case 'like':
case 'LIKE_IN_COMMENT':
return (
<ThumbsIcon
stroke={colors.white_40}
Expand All @@ -42,27 +42,47 @@ const NotificationItem = ({ onClick, notification }: NotificationItem) => {
}
};

const handleNotificationClick = () => {
if (!notification.isRead) {
readNotification.mutate();
}
};

return (
<Row
onClick={onClick}
onClick={handleNotificationClick}
justifyContent="space-between"
padding={'24px 20px'}
style={{ ...(!notification.checked && { backgroundColor: '#2e234a' }) }}
style={{ ...(!notification.isRead && { backgroundColor: '#2e234a' }), position: 'relative' }}
gap={28}
>
<Row gap={16}>
{!notification.isRead && (
<div
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: colors.purple2,
position: 'absolute',
left: 18,
top: 18,
}}
/>
)}

<IconWrapper>{renderIcon()}</IconWrapper>
<Col gap={8}>
<Text size={15} weight={500} color={colors.white}>
투표가 마감 되었어요, 지금 바로 결과를 확인해 보세요!
{notification.message.title}
</Text>
<Text size={14} weight={400} color={colors.purple2}>
성수 치킨 버거의 종결지는? 성수 치킨 버거의 종결지는?
{notification.message.content}
</Text>
</Col>
</Row>
<Text size={13} weight={400} color={colors.white_40} style={{ flexShrink: 0 }}>
방금
{getDateDiff(notification.createdAt)}
</Text>
</Row>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import React from 'react';
import { useNavigate } from 'react-router-dom';
import { styled } from 'styled-components';

import { useUnreadNotifications } from '@apis/notification/useNotifications';

import { AlarmIcon } from '@icons/index';

const NotificationButton = () => {
const navigate = useNavigate();
const unreadNotifications = useUnreadNotifications();

return (
<AlarmButton onClick={() => navigate('/notifications')}>
Expand Down
17 changes: 17 additions & 0 deletions src/interfaces/api/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
interface NotificationResponse {
id: number;
type: 'VOTE_RESULT' | 'VOTE_COUNT_ON_TOPIC' | 'COMMENT_ON_TOPIC' | 'LIKE_IN_COMMENT';
receiverType: string;
isRead: boolean;
message: Message;
createdAt: number;
}

interface Message {
title: string;
content: string;
topicId: number;
commentId: number;
}

export type { NotificationResponse, Message };
66 changes: 5 additions & 61 deletions src/routes/Notification/Notification.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState } from 'react';

import { useNotifications } from '@apis/notification/useNotifications';
import { Col } from '@components/commons/Flex/Flex';
import BackButton from '@components/commons/Header/BackButton/BackButton';
import Layout from '@components/commons/Layout/Layout';
Expand All @@ -17,56 +18,7 @@ const Notification = () => {
const [currentTab, setCurrentTab] = useState<(typeof NOTIFICATIONS_TABS)[number]>(
NOTIFICATIONS_TABS[0]
);

const notifications: Array<{
type: 'hit' | 'comment' | 'like' | 'close';
title: string;
date: number;
checked: boolean;
}> = [
{
// 투표가 마감 되었어요, 지금 바로 결과를 확인해 보세요!
type: 'close',
title: '성수 치킨 버거의 종결지는? 성수 치킨 버거의 종결지는?',
date: 1800000000,
checked: false,
},
{
// 다른 사용자가 내 댓글에 좋아요를 눌렀어요.
type: 'like',
title: '강아지상? 고양이상?',
date: 1803204000,
checked: false,
},
{
// 내가 만든 토픽에 누군가가 댓글을 남겼어요 바로 확인해 볼까요?
type: 'comment',
title: '강아지상? 고양이상?',
date: 1803204000,
checked: true,
},
{
// 내가 만든 토픽의 투표수가 {#100단위}을 돌파했어요!
type: 'hit',
title: '강아지상? 고양이상?',
date: 1803204000,
checked: true,
},
{
// 내가 만든 토픽에 누군가가 댓글을 남겼어요 바로 확인해 볼까요?
type: 'comment',
title: '강아지상? 고양이상?',
date: 1803244000,
checked: false,
},
{
// 투표가 마감 되었어요, 지금 바로 결과를 확인해 보세요!
type: 'close',
title: '성수 치킨 버거의 종결지는? 성수 치킨 버거의 종결지는?',
date: 1800260000,
checked: true,
},
];
const { data: notifications } = useNotifications();

return (
<Layout
Expand All @@ -79,18 +31,10 @@ const Notification = () => {
}
>
<Container>
<TabHeader currentTab={currentTab} setCurrentTab={setCurrentTab} />
{/* <TabHeader currentTab={currentTab} setCurrentTab={setCurrentTab} /> */}
<Col style={{ overflowY: 'auto', height: 'fill-available' }}>
{notifications.map((notification, index) => {
return (
<NotificationItem
key={index}
notification={notification}
onClick={function (): void {
throw new Error('Function not implemented.');
}}
/>
);
{notifications?.map((notification, index) => {
return <NotificationItem key={index} notification={notification} />;
})}
</Col>
</Container>
Expand Down

0 comments on commit 2c5ff25

Please sign in to comment.