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

알림창, 마이페이지 드롭다운 공용 컴포넌트 구현 + 알림 기능 추가 #613

Merged
merged 39 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4a6c967
refactor: 러너, 서포터 마이페이지 분리
gyeongza Sep 30, 2023
549945c
assets: 알림 아이콘 에셋 추가
gyeongza Oct 2, 2023
258eeaf
feat: Dropdown 공용 컴포넌트 구현
gyeongza Oct 2, 2023
096bc90
feat: 알림 컴포넌트 구현
gyeongza Oct 2, 2023
21a6538
design: 드롭다운 컴포넌트 z-index 추가
gyeongza Oct 2, 2023
91c5f73
assets: 마이페이지, 로그아웃 아이콘 에셋 추가
gyeongza Oct 2, 2023
d2674f4
feat: 드롭다운 메뉴와 트리거와의 gap 설정 props 추가
gyeongza Oct 2, 2023
47b3cfb
design: 마지막 li border-radius 추가
gyeongza Oct 2, 2023
8bae896
feat: 드롭다운 컴포넌트 esc, backdrop 클릭 닫기 구현
gyeongza Oct 2, 2023
1678344
feat: 러너, 서포터 마이페이지 라우터 추가
gyeongza Oct 2, 2023
d848783
design: 알림창 반응형 추가
gyeongza Oct 2, 2023
6bc267d
feat: 프로필 드롭다운 컴포넌트 구현
gyeongza Oct 2, 2023
98ab501
feat: 알림, 프로필 클릭 시 드롭다운 되도록 변경
gyeongza Oct 2, 2023
21b14df
refactor: button 태그를 p태그로 변경
gyeongza Oct 2, 2023
ea3d20e
fix: 메인 페이지 이외에서 로그아웃시 권한오류 뜨는 것 수정
gyeongza Oct 2, 2023
bc7c971
chore: cypress open 스크립트 추가
gyeongza Oct 3, 2023
f16d4ac
test: 러너, 서포터 마이페이지 렌더링 테스트 코드 작성
gyeongza Oct 3, 2023
2e2638e
feat: 드롭다운 컴포넌트 스토리북 추가
gyeongza Oct 3, 2023
14b21e8
fix: 글 생성 완료 페이지에서 러너 마이페이지로 가도록 변경
gyeongza Oct 3, 2023
8fa8dea
fix: ci 테스트 실패 오류 수정 (명령어 체인 분리)
gyeongza Oct 3, 2023
2917bd4
design: 버튼 기본 스타일 제거
gyeongza Oct 4, 2023
015e155
design: 삭제 및 스크롤 추가
gyeongza Oct 4, 2023
32a285b
feat: 알림 타입 및 게시물로 이동 이벤트 추가
gyeongza Oct 4, 2023
e1001f1
Merge branch 'dev/FE' into feat/601
gyeongza Oct 5, 2023
39139b2
fix: 분리된 마이페이지로 라우팅 변경
gyeongza Oct 5, 2023
cf9aa30
feat: 러너, 서포터 마이페이지 리액트 쿼리 마이그레이션
gyeongza Oct 5, 2023
d34ec20
style: 불필요한 공백 삭제
gyeongza Oct 5, 2023
97cd833
refactor: Notification -> Alarm으로 네이밍 변경
gyeongza Oct 6, 2023
063b286
feat: 알람 불러오기 및 삭제 기능 추가
gyeongza Oct 6, 2023
d9312bc
feat: 알림 읽음 patch 구현
gyeongza Oct 6, 2023
faaf494
feat: 알람이 없는 경우 빈 알람 아이콘 추가
gyeongza Oct 6, 2023
4180ce0
feat: 회원탈퇴 버튼 추가
gyeongza Oct 6, 2023
f331663
fix: 아이콘 오타 수정
gyeongza Oct 6, 2023
94c5961
refactor: notification으로 네이밍 변경
gyeongza Oct 10, 2023
36d5fa3
refactor: 콜백함수 제거 및 네이밍 변경
gyeongza Oct 10, 2023
f7a3eef
refactor: 불필요한 async 제거
gyeongza Oct 10, 2023
725f019
refactor: 알람 인자로 isLogin 추가
gyeongza Oct 10, 2023
8ae21f1
design: 알람 읽음 표시 추가
gyeongza Oct 10, 2023
540d494
refactor: isLogin 인자 제거
gyeongza Oct 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions frontend/cypress/e2e/app.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ describe('러너 E2E 테스트', () => {

cy.get('div[aria-label="에러 메시지"]').should('be.visible');

cy.get('input[value="주소주소"]').clear().type('https://github.com/woowacourse-teams/2023-baton/pull/1');
cy.get('input[value="주소주소"]').clear();
cy.get('input[value=""]').type('https://github.com/woowacourse-teams/2023-baton/pull/1');

cy.get('button[aria-label="리뷰 요청 글 생성"]').click();

Expand All @@ -78,11 +79,17 @@ describe('러너 E2E 테스트', () => {
cy.get('div[aria-label="알림 메시지"]').should('be.visible');
});

it('마이페이지 게시글을 불러온다', () => {
it('러너 마이페이지 대기중인 리뷰 게시글을 불러온다', () => {
cy.get('img[alt="프로필"]').click();

cy.wait(500);

cy.get('li[aria-label="러너 마이페이지"]').should('be.visible');
cy.get('li[aria-label="서포터 마이페이지"]').should('be.visible');
cy.get('li[aria-label="로그아웃"]').should('be.visible');

cy.get('li[aria-label="러너 마이페이지"]').click();

cy.contains('더보기').click();

cy.wait(500);
Expand All @@ -102,12 +109,17 @@ describe('러너 E2E 테스트', () => {
});
});

it('마이페이지 추가 게시글을 불러온다.', () => {
it('서포터 마이페이지 진행중인 리뷰 게시글을 불러온다.', () => {
cy.get('img[alt="프로필"]').click();

cy.wait(500);

cy.contains('button', '서포터').click();
cy.get('li[aria-label="러너 마이페이지"]').should('be.visible');
cy.get('li[aria-label="서포터 마이페이지"]').should('be.visible');
cy.get('li[aria-label="로그아웃"]').should('be.visible');

cy.get('li[aria-label="서포터 마이페이지"]').click();

cy.contains('button', '진행중인 리뷰').click();

cy.wait(500);
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "webpack-dev-server --config webpack/webpack.dev.js",
"build": "webpack --config webpack/webpack.prod.js",
"test": "cypress run",
"cypress": "cypress open",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"chromatic": "npx chromatic --project-token=chpt_2be44b6342a5cae"
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/apis/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { GetMyPagePostResponse } from '@/types/myPage';
import { PostFeedbackRequest } from '@/types/feedback';
import { GetSupporterCandidateResponse } from '@/types/supporterCandidate';
import { GetAlarmResponse } from '@/types/alarm';

export const getRunnerPost = (limit: number, reviewStatus?: ReviewStatus, cursor?: number, tagName?: string) => {
const params = new URLSearchParams({
Expand Down Expand Up @@ -72,6 +73,10 @@ export const getRunnerPostDetail = (runnerPostId: number, isLogin: boolean) => {
return request.get<GetDetailedRunnerPostResponse>(`/posts/runner/${runnerPostId}`, isLogin);
};

export const getAlram = (isLogin: boolean) => {
return request.get<GetAlarmResponse>(`/alarms`, isLogin);
};

export const postRunnerPostCreation = (formData: CreateRunnerPostRequest) => {
const body = JSON.stringify(formData);
return request.post<void>(`/posts/runner`, body);
Expand Down Expand Up @@ -110,10 +115,18 @@ export const patchProposedSupporterSelection = (runnerPostId: number, supporterI
return request.patch<void>(`/posts/runner/${runnerPostId}/supporters`, body);
};

export const patchAlarmCheck = (alarmId: number) => {
return request.patch<void>(`/alarms/${alarmId}`, undefined);
};

export const deleteRunnerPost = (runnerPostId: number) => {
return request.delete<void>(`/posts/runner/${runnerPostId}`);
};

export const deleteAlarm = (alarmsId: number) => {
return request.delete<void>(`/alarms/${alarmsId}`);
};

export const postMissionBranchCreation = (repoName: string) => {
const body = JSON.stringify({ repoName });
return request.post<void>(`/branch`, body);
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/assets/alarm_off.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions frontend/src/assets/alarm_on.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/logout-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/my-page-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
153 changes: 153 additions & 0 deletions frontend/src/components/AlarmDropdown/AlarmDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React from 'react';
import styled from 'styled-components';
import { Alarm } from '@/types/alarm';
import { usePageRouter } from '@/hooks/usePageRouter';
import { useAlarmDelete } from '@/hooks/query/useAlarmDelete';
import { useAlarmCheck } from '@/hooks/query/useAlarmCheck';

interface Props {
alarmList: Alarm[];
}

const AlarmDropdown = ({ alarmList }: Props) => {
const { mutate: deleteAlarm } = useAlarmDelete();
const { mutate: patchAlarmCheck } = useAlarmCheck();
const { goToRunnerPostPage } = usePageRouter();

const handlePostClick = (alarmId: number, runnerPostId: number) => {
patchAlarmCheck(alarmId);
goToRunnerPostPage(runnerPostId);
};

const handleDeleteAlarm = (e: React.MouseEvent, alarmId: number) => {
e.stopPropagation();

deleteAlarm(alarmId);
};

return (
<S.DropdownContainer>
{alarmList?.length > 0 ? (
alarmList?.map((alarm) => {
return (
<S.DropdownList key={alarm.alarmId} onClick={() => handlePostClick(alarm.alarmId, alarm.referencedId)}>
<S.AlarmTitleContainer>
<S.AlarmTitle>{alarm.title}</S.AlarmTitle>
<S.CloseButton onClick={(e) => handleDeleteAlarm(e, alarm.alarmId)}>삭제</S.CloseButton>
</S.AlarmTitleContainer>
<S.AlarmContents>{alarm.message}</S.AlarmContents>
<S.AlarmTime>{alarm.createdAt}</S.AlarmTime>
</S.DropdownList>
);
})
) : (
<S.EmptyMessage>새로운 알림이 없습니다.</S.EmptyMessage>
)}
</S.DropdownContainer>
);
};

export default AlarmDropdown;

const S = {
DropdownContainer: styled.ul`
width: 414px;
max-height: 427px;

overflow-y: scroll;

& > li {
border-bottom: 1px solid var(--gray-400);
}

& > li:last-child {
border-bottom: none;
border-radius: 0 0 10px 10px;
}

@media (max-width: 768px) {
width: 290px;
}
`,

DropdownList: styled.li`
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px 35px;

cursor: pointer;

&:hover {
background-color: var(--gray-100);
}
`,

AlarmTitleContainer: styled.div`
display: flex;
justify-content: space-between;
`,

CloseButton: styled.button`
font-size: 12px;

&:hover {
color: var(--baton-red);
}

@media (max-width: 768px) {
font-size: 10px;
}
`,

AlarmTitle: styled.p`
font-size: 16px;
font-weight: 700;
position: relative;

&::before {
content: '';
position: absolute;
width: 4px;
height: 4px;
left: -10px;
top: 50%;
transform: translateY(-50%);

background-color: var(--baton-red);
border-radius: 50%;
}

@media (max-width: 768px) {
font-size: 14px;
}
`,

AlarmContents: styled.p`
font-size: 14px;
color: var(--gray-700);

@media (max-width: 768px) {
font-size: 12px;
}
`,

AlarmTime: styled.p`
font-size: 12px;
color: var(--gray-700);

text-align: end;

@media (max-width: 768px) {
font-size: 10px;
}
`,

EmptyMessage: styled.p`
height: 427px;

display: flex;
justify-content: center;
align-items: center;
`,
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useReviewCancelation } from '@/hooks/query/useReviewCancelation';
import { useReviewComplete } from '@/hooks/query/useReviewComplete';
import { usePageRouter } from '@/hooks/usePageRouter';
import useViewport from '@/hooks/useViewport';

import { ReviewStatus } from '@/types/runnerPost';
import React, { useContext } from 'react';
import styled from 'styled-components';
Expand Down Expand Up @@ -42,11 +41,13 @@ const MyPagePostButton = ({ runnerPostId, reviewStatus, isRunner, supporterId, a

const handleClickSupportSelectButton = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();

goToSupportSelectPage(runnerPostId);
};

const handleClickSupportFeedbackButton = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();

if (!supporterId) {
showErrorToast({ title: ERROR_TITLE.ERROR, description: ERROR_DESCRIPTION.NO_SUPPORTER });
return;
Expand Down
77 changes: 77 additions & 0 deletions frontend/src/components/ProfileDropdown/ProfileDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import styled from 'styled-components';
import MyPageIcon from '@/assets/my-page-icon.svg';
import LogoutIcon from '@/assets/logout-icon.svg';
import { useLogin } from '@/hooks/useLogin';
import { usePageRouter } from '@/hooks/usePageRouter';

const ProfileDropdown = () => {
const { logout } = useLogin();
const { goToMainPage, goToRunnerMyPage, goToSupporterMyPage } = usePageRouter();

const handleClickRunnerMyPage = () => {
goToRunnerMyPage();
};

const handleClickSupporterMyPage = () => {
goToSupporterMyPage();
};

const handleClickLogoutButton = () => {
logout();

goToMainPage();
window.location.reload();
};

return (
<S.DropdownContainer>
<S.DropdownList aria-label="러너 마이페이지" onClick={handleClickRunnerMyPage}>
<img width="18px" height="18px" src={MyPageIcon} />
<S.DropdownListTitle>러너 마이페이지</S.DropdownListTitle>
</S.DropdownList>
<S.DropdownList aria-label="서포터 마이페이지" onClick={handleClickSupporterMyPage}>
<img width="18px" height="18px" src={MyPageIcon} />
<S.DropdownListTitle>서포터 마이페이지</S.DropdownListTitle>
</S.DropdownList>
<S.DropdownList aria-label="로그아웃" onClick={handleClickLogoutButton}>
<img width="18px" height="18px" src={LogoutIcon} />
<S.DropdownListTitle>로그아웃</S.DropdownListTitle>
</S.DropdownList>
</S.DropdownContainer>
);
};

export default ProfileDropdown;

const S = {
DropdownContainer: styled.ul`
width: max-content;
height: max-content;

& > li {
border-bottom: 1px solid var(--gray-400);
}

& > li:last-child {
border-bottom: none;
border-radius: 0 0 10px 10px;
}
`,

DropdownList: styled.li`
display: flex;
align-items: center;
gap: 12px;

padding: 15px 25px;

cursor: pointer;

&:hover {
background-color: var(--gray-100);
}
`,

DropdownListTitle: styled.p``,
};
32 changes: 32 additions & 0 deletions frontend/src/components/common/Dropdown/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import Dropdown from './Dropdown';
import Button from '../Button/Button';

const meta = {
title: 'common/Dropdown',
component: Dropdown,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof Dropdown>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {
trigger: <Button colorTheme="RED">Button</Button>,
children: (
<ul style={{ display: 'flex', flexDirection: 'column', gap: '10px', width: '180px', padding: '10px' }}>
<li>첫 번째 리스트</li>
<li>두 번째 리스트</li>
<li>세 번째 리스트</li>
</ul>
),
isDropdownOpen: true,
gapFromTrigger: '40px',
onClose: () => {},
},
};
Loading