Skip to content

Commit

Permalink
활동 탭 > Heatmap 상세 정보 구현 (#206)
Browse files Browse the repository at this point in the history
* ✨ #205 - 날짜별 heatmap 데이터 조회 커스텀 훅 생성

- 타입 정의 및 api 구현

* 🩹 #205 - dateFormat 공백 제거

* 💡 #205 - Formatter 함수에 js doc 적용

* ✨ #205 - YYYY.MM.DD Day of week 형식 날짜 포맷 함수 생성

* ✨ #205 - 날짜별 활동 내역을 조회하기 위한 YYYY-MM-DD 형식 날짜 포맷 함수 생성

* ✨ #205 - 활동 내역 상세  컴포넌트 생성

* ✨ #205 - 히트맵 셀 클릭 시 해당 날짜 활동 내역 상세 노출 구현

* 🩹 #205 - 잘못된 import 수정

* ♻️ #205 - 상수 정의 및 적용

* 🔥 #205 - 불필요한 import 제거

* 🩹 #205 - 활동탭 진입 시 날짜 선택하지 않은 상태로 변경

* 💄 #205 - 활동 내역 상세 로딩 UI 적용

* 💄 #205 - 활동 달력 내 폰트 수정

* 💄 #205 - hover 스타일 제거 및 class name 추가

* 🔥 #205 - 불필요한 코드 제거

* 👽️ #205 - API endpoint 및 queryKeys 수정

* 🔧 #205 - tsconfig 수정

* 🔥 #205 - 불필요한 코드 및 파일 삭제

* 🐛 #205 - build error 해결

* 🚚 #205 - HeatmapCalendar > ActivitiesContainer 파일명 변경

* ♻️ #205 - ActivitiesCalendar 컴포넌트 생성 및 적용

* 🚚 #205 - HeatmapDetail > ActivityDetail 파일명 변경

* ♻️ #205 - ActivityDiariesContainer 컴포넌트 생성 및 적용

* 🚚 #205 - types/heatmap > types/activity 파일명 변경 및 타입 명 변경

* 🚚 #205 - api/heatmap > api/activities 파일명 변경 및 api 명 변경

* 🚚 #205 - 활동 탭 관련 커스텀 훅 및 컴포넌트 props 네이밍 변경

* ✨ #205 - 활동 탭 진입 시 오늘 날짜 활성화 되도록 적용
  • Loading branch information
Bori-github authored Mar 17, 2024
1 parent 72eb75f commit de60e7a
Show file tree
Hide file tree
Showing 30 changed files with 431 additions and 132 deletions.
32 changes: 32 additions & 0 deletions src/api/activities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type {
GetActivitiesByUsernameRequest,
GetActivityDetailRequest,
Activity,
ActivityDetail,
} from 'types/activity';
import type { SuccessResponse } from 'types/response';
import { API_PATH } from 'constants/services';
import axios from 'lib/axios';

export const getActivitiesByUsername = async ({
username,
}: GetActivitiesByUsernameRequest) => {
const {
data: { data },
} = await axios.get<SuccessResponse<Activity[]>>(
`${API_PATH.activities.index}/${username}`,
);
return data;
};

export const getActivityDetail = async ({
username,
dateString,
}: GetActivityDetailRequest) => {
const {
data: { data },
} = await axios.get<SuccessResponse<ActivityDetail>>(
`${API_PATH.activities.index}/${username}/${dateString}`,
);
return data;
};
15 changes: 0 additions & 15 deletions src/api/heatmap.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export * from './comments';
export * from './favorite';
export * from './bookmark';
export * from './profile';
export * from './heatmap';
export * from './activities';
1 change: 0 additions & 1 deletion src/components/account/CompleteFindPassword.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import styled from '@emotion/styled';
import Image from 'next/image';
import React from 'react';

export const CompleteFindPassword = () => {
return (
Expand Down
1 change: 0 additions & 1 deletion src/components/account/RegisterInformation.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import styled from '@emotion/styled';
import { isAxiosError } from 'axios';
import React from 'react';
import { useFormContext } from 'react-hook-form';
import type { RegisterStep, RegisterForm } from 'types/register';
import type { ErrorResponse } from 'types/response';
Expand Down
42 changes: 42 additions & 0 deletions src/components/diary/ActivityDiariesContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import styled from '@emotion/styled';
import Diary from './Diary';
import type { DiaryDetail } from 'types/diary';
import { ScreenReaderOnly } from 'styles';

interface ActivityDiariesContainerProps {
title: string;
diariesData: DiaryDetail[];
empty: JSX.Element;
}

export const ActivityDiariesContainer = ({
title,
diariesData,
empty,
}: ActivityDiariesContainerProps) => {
const isEmptyDiaries = diariesData.length === 0;

if (isEmptyDiaries) return empty;

return (
<section>
<Title>{title}</Title>
<List>
{diariesData.map((diary) => {
const { id } = diary;
return <Diary key={`diary-list-${id}`} {...diary} />;
})}
</List>
</section>
);
};

const Title = styled.h2`
${ScreenReaderOnly}
`;

const List = styled.ul`
display: grid;
gap: 6px;
background-color: ${({ theme }) => theme.colors.gray_06};
`;
40 changes: 40 additions & 0 deletions src/components/diary/EmptyActivitiesDiary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import styled from '@emotion/styled';
import { useRouter } from 'next/router';
import { Button } from 'components/common';
import { PAGE_PATH } from 'constants/common';

export const EmptyActivitiesDiary = () => {
const router = useRouter();

const handleGoToWriteDiary = () => {
void router.push(PAGE_PATH.diary);
};

return (
<EmptyContainer>
<EmptyTextContainer>
<p>일기가 없습니다.</p>
<p>오늘 일기를 작성해보세요.</p>
</EmptyTextContainer>
<Button
text="일기 작성하러 가기"
size="sm"
onClick={handleGoToWriteDiary}
/>
</EmptyContainer>
);
};

const EmptyContainer = styled.div`
padding: 50px;
text-align: center;
`;

const EmptyTextContainer = styled.div`
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 16px;
color: ${({ theme }) => theme.colors.gray_02};
${({ theme }) => theme.fonts.body_08};
`;
2 changes: 2 additions & 0 deletions src/components/diary/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export * from './Diary';
export * from './DiariesContainer';
export * from './DiaryDetailContainer';
export * from './EmptyDiary';
export * from './EmptyActivitiesDiary';
export * from './ActivityDiariesContainer';
Original file line number Diff line number Diff line change
@@ -1,40 +1,48 @@
import styled from '@emotion/styled';
import { useEffect, useRef } from 'react';
import CalendarHeatmap from 'react-calendar-heatmap';
import type { HeatmapCell } from 'types/heatmap';
import { WEEKDAY } from 'constants/common';
import type { Activity } from 'types/activity';
import { DAY_OF_WEEK } from 'constants/common';
import { HEATMAP_WIDTH } from 'constants/styles';
import { getLastYearDate } from 'utils';

interface HeatmapCalendarProps {
heatmapCalendarData: HeatmapCell[];
interface ActivitiesCalendarProps {
activitiesData: Activity[];
selectedDate: string;
onClick: (value: Activity) => void;
}

export const HeatmapCalendar = ({
heatmapCalendarData,
}: HeatmapCalendarProps) => {
export const ActivitiesCalendar = ({
activitiesData,
selectedDate,
onClick,
}: ActivitiesCalendarProps) => {
const today = new Date();

const boxRef = useRef<HTMLDivElement | null>(null);

const getClassForValue = (value: HeatmapCell) => {
const { activityCount } = value;
const getClassForValue = (value: Activity) => {
if (value === null) return;

const { activityCount, date } = value;
const isSelected = date === selectedDate;
let className = '';

switch (true) {
case activityCount > 4:
return 'color-step-3';
className = 'color-step-3';
break;
case activityCount > 2:
return 'color-step-2';
className = 'color-step-2';
break;
case activityCount > 0:
return 'color-step-1';
className = 'color-step-1';
break;
default:
return 'color-step-0';
className = 'color-step-0';
}
};

const handleClick = (value: HeatmapCell) => {
// TODO: 클릭 시 해당 날짜 활동 내역 보여주기
console.log(`Clicked on value with count: ${value.activityCount}`);
return isSelected ? `${className} selected` : className;
};

useEffect(() => {
Expand All @@ -44,40 +52,33 @@ export const HeatmapCalendar = ({
}, []);

return (
<Container>
<Contents ref={boxRef}>
<WeekdayList>
{WEEKDAY.map((day) => (
<li key={day}>{day}</li>
))}
</WeekdayList>

<CalendarContainer>
<CalendarHeatmap
gutterSize={1}
startDate={getLastYearDate(today)}
endDate={today}
values={heatmapCalendarData}
classForValue={(value: HeatmapCell) => getClassForValue(value)}
onClick={(value: HeatmapCell) => {
handleClick(value);
}}
/>
</CalendarContainer>
</Contents>
<Container ref={boxRef}>
<WeekdayList>
{DAY_OF_WEEK.short.map((day) => (
<li key={day}>{day}</li>
))}
</WeekdayList>

<CalendarContainer>
<CalendarHeatmap
gutterSize={1}
startDate={getLastYearDate(today)}
endDate={today}
values={activitiesData}
classForValue={getClassForValue}
onClick={onClick}
/>
</CalendarContainer>
</Container>
);
};

const Container = styled.div`
padding: 0 20px;
`;

const Contents = styled.div`
overflow-x: auto;
display: grid;
grid-template-columns: 28px auto;
gap: 2px;
margin: 0 20px;
scrollbar-width: none;
-ms-overflow-style: none;
Expand All @@ -97,10 +98,7 @@ const WeekdayList = styled.ul`
bottom: 0;
padding-bottom: 8px;
background-color: ${({ theme }) => theme.colors.white};
font-size: 1rem;
font-weight: 400;
letter-spacing: -0.04em;
line-height: 26px;
${({ theme }) => theme.fonts.caption_03}
`;

const CalendarContainer = styled.div`
Expand All @@ -112,15 +110,15 @@ const CalendarContainer = styled.div`
}
& .react-calendar-heatmap text {
font-family: pretendard;
font-size: 5.5px;
${({ theme }) => theme.fonts.caption_01};
font-size: 0.55rem;
}
& .react-calendar-heatmap rect {
rx: 1px;
}
& .react-calendar-heatmap rect:hover {
& .react-calendar-heatmap .selected {
border-radius: 0.5px;
outline: 1px solid ${({ theme }) => theme.colors.pink};
outline-offset: -1px;
Expand Down
36 changes: 36 additions & 0 deletions src/components/profile/ActivitiesContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState } from 'react';
import { ActivitiesCalendar } from './ActivitiesCalendar';
import { ActivityDetail } from './ActivityDetail';
import type { Activity } from 'types/activity';
import { dateStringFormat } from 'utils';

interface ActivitiesContainerProps {
activitiesData: Activity[];
}

export const ActivitiesContainer = ({
activitiesData,
}: ActivitiesContainerProps) => {
const todayDateString = dateStringFormat(new Date().toDateString()) as string;
const [selectedDate, setSelectedDate] = useState<string>(todayDateString);

const isSelected = selectedDate.length !== 0;

const handleClick = (value: Activity) => {
const { date } = value;

setSelectedDate(dateStringFormat(date) as string);
};

return (
<section>
<ActivitiesCalendar
activitiesData={activitiesData}
selectedDate={selectedDate}
onClick={handleClick}
/>

{isSelected && <ActivityDetail dateString={selectedDate} />}
</section>
);
};
Loading

0 comments on commit de60e7a

Please sign in to comment.