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

Feat[#173]: 모집상세 페이지의 리뷰 리스트 구현 #189

Merged
merged 14 commits into from
Mar 25, 2024
Merged
2 changes: 1 addition & 1 deletion src/components/commons/ReviewCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const ReviewCard = ({ user, rating, createdAt, content }: ReviewCardProps) => {
<div className={cx('review-card-info')}>
<h1 className={cx('review-card-info-reviewer')}>{user.nickname}</h1>
<div className={cx('review-card-info-rating')}>
<StarRating size='small' rating={rating} />
<StarRating size='small' rating={rating} readonly />
<span className={cx('review-card-info-date')}>{getFormatDate(createdAt)}</span>
</div>
</div>
Expand Down
47 changes: 47 additions & 0 deletions src/components/postDetail/ReviewList/ReviewList.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.review-list {
@include column-flexbox($gap: 2.4rem);

width: 100%;

&-header {
width: 100%;

&-count-group {
@include flexbox(between);

height: 6.4rem;

.summary {
@include flexbox($gap: 0.8rem);

.title {
@include text-style(16, $white);
}

.count {
@include text-style(14, $primary, bold);
}
}

.dropdown {
width: 13rem;
}
}
}

&-review-group {
@include column-flexbox($gap: 2.4rem);

width: 100%;

.item-list {
@include column-flexbox(center, start, 1.6rem);

width: 100%;

.item {
width: 100%;
}
}
}
}
90 changes: 90 additions & 0 deletions src/components/postDetail/ReviewList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useState } from 'react';

import classNames from 'classnames/bind';

import { REVIEW_SORT_OPTIONS } from '@/constants';
import { getReviewPageSize } from '@/utils/getPageSize';

import Dropdown from '@/components/commons/Dropdown';
import Pagination from '@/components/commons/Pagination';
import ReviewCard from '@/components/commons/ReviewCard';
import ReviewSummary from '@/components/postDetail/ReviewSummary';
import { REVIEW_LIST_DATA } from '@/constants/mockData/reviewList';
import { useDeviceType } from '@/hooks/useDeviceType';
import usePaginatedDataList from '@/hooks/usePaginatedDataList';
import useSortedDataList from '@/hooks/useSortedDataList';

import { Order, Review, ReviewResponse, SortOption } from '@/types';

import styles from './ReviewList.module.scss';

const cx = classNames.bind(styles);

type ReviewListProps = {
list?: ReviewResponse;
};

const ReviewList = ({ list = REVIEW_LIST_DATA }: ReviewListProps) => {
const [page, setPage] = useState(1);

const currentDeviceType = useDeviceType();
const pageSize = getReviewPageSize(currentDeviceType);

const initialDataList: Review[] = list.reviews;
grapefruit13 marked this conversation as resolved.
Show resolved Hide resolved
const initialSortOption: SortOption<Review> = {
key: 'rating',
order: 'desc',
type: 'number',
};

const [sortOption, setSortOption] = useState(initialSortOption);

const sortedDataList = useSortedDataList({
initialDataList,
sortOption,
});

const pagedDataList = usePaginatedDataList({
initialDataList: sortedDataList,
page,
setPage,
postsPerPage: pageSize,
});

const handleOptionChange = (value: string | number) => {
setSortOption({ key: 'rating', order: value as Order, type: 'number' });
grapefruit13 marked this conversation as resolved.
Show resolved Hide resolved
};

const handleClickPage = (pageNumber: number) => {
setPage(pageNumber);
};

return (
<article className={cx('review-list')}>
<header className={cx('review-list-header')}>
<div className={cx('review-list-header-count-group')}>
<div className={cx('summary')}>
<span className={cx('title')}>리뷰</span>
<span className={cx('count')}>{list.totalCount}</span>
</div>
<div className={cx('dropdown')}>
<Dropdown options={REVIEW_SORT_OPTIONS} onChange={handleOptionChange} isSmall color='yellow' />
</div>
</div>
<ReviewSummary rating={list.averageRating} nickname='주인장' email='[email protected]' />
</header>
<div className={cx('review-list-review-group')}>
<ul className={cx('item-list')}>
{pagedDataList.map((item) => (
<li className={cx('item')} key={item.id}>
<ReviewCard user={item.user} rating={item.rating} createdAt={item.createdAt} content={item.content} />
</li>
))}
</ul>
<Pagination totalCount={list.totalCount} pageState={page} postPerPage={pageSize} onClick={handleClickPage} />
</div>
</article>
);
};

export default ReviewList;
64 changes: 64 additions & 0 deletions src/components/postDetail/ReviewSummary/ReviewSummary.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.review-summary {
@include flexbox($gap: 17.2rem);

@include responsive(M) {
flex-direction: column;
gap: 0;
}

width: 100%;
padding: 2.7rem 0;
background: $black80;
border-radius: 0.4rem;

&-profile {
@include flexbox($gap: 1.2rem);

@include responsive(M) {
width: 100%;
padding-bottom: 2.4rem;
border-bottom: 0.1rem solid $black50;

&::after {
display: none;
}
}

position: relative;

&::after {
content: '';

position: absolute;
top: 0;
right: -8.6rem;
bottom: 0;

border: 0.1rem solid $black50;
}

&-info {
@include column-flexbox(center, start);

.nickname {
@include text-style(16, $white);
}

.email {
@include text-style(14, $gray30);
}
}
}

&-star {
@include flexbox($gap: 0.8rem);

@include responsive(M) {
padding-top: 2.4rem;
}

.rating {
@include text-style(20, $white, bold);
}
}
}
37 changes: 37 additions & 0 deletions src/components/postDetail/ReviewSummary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import classNames from 'classnames/bind';

import { toFixedOneDecimal } from '@/utils';

import Avatar from '@/components/commons/Avatar';
import StarRating from '@/components/commons/StarRating';
import { USER_DATA } from '@/constants/mockData/headerMockData';

import styles from './ReviewSummary.module.scss';

const cx = classNames.bind(styles);

type ReviewSummary = {
rating: number;
nickname: string;
email: string;
};

const ReviewSummary = ({ rating, nickname, email }: ReviewSummary) => {
return (
<div className={cx('review-summary')}>
<div className={cx('review-summary-profile')}>
<Avatar size='medium' profileImageUrl={USER_DATA.profileImageUrl} />
<div className={cx('review-summary-profile-info')}>
<span className={cx('nickname')}>{nickname}</span>
<span className={cx('email')}>{email}</span>
</div>
</div>
<div className={cx('review-summary-star')}>
<StarRating size='medium' rating={rating} readonly />
<span className={cx('rating')}>{toFixedOneDecimal(rating)}</span>
</div>
</div>
);
};

export default ReviewSummary;
86 changes: 86 additions & 0 deletions src/constants/mockData/reviewList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { USER_DATA } from './headerMockData';

export const REVIEW_LIST_DATA = {
averageRating: 4,
totalCount: 6,
reviews: [
{
id: 1,
user: {
profileImageUrl: USER_DATA.profileImageUrl,
nickname: USER_DATA.nickname,
id: USER_DATA.id,
},
activityId: 1,
rating: 1,
content: '너무 재밌게 게임했습니다. 다음에 또 하고 싶어요. 최곱니다!!!',
createdAt: '2024-03-22T15:24:25.173Z',
updatedAt: '2024-03-23T15:24:25.173Z',
},
{
id: 2,
user: {
profileImageUrl: USER_DATA.profileImageUrl,
nickname: USER_DATA.nickname,
id: USER_DATA.id,
},
activityId: 1,
rating: 5,
content: '너무 재밌게 게임했습니다. 다음에 또 하고 싶어요. 최곱니다!!!',
createdAt: '2024-03-23T15:24:25.173Z',
updatedAt: '2024-03-23T15:24:25.173Z',
},
{
id: 3,
user: {
profileImageUrl: USER_DATA.profileImageUrl,
nickname: USER_DATA.nickname,
id: USER_DATA.id,
},
activityId: 1,
rating: 3,
content: '너무 재밌게 게임했습니다. 다음에 또 하고 싶어요. 최곱니다!!!',
createdAt: '2024-03-24T15:24:25.173Z',
updatedAt: '2024-03-23T15:24:25.173Z',
},
{
id: 4,
user: {
profileImageUrl: USER_DATA.profileImageUrl,
nickname: USER_DATA.nickname,
id: USER_DATA.id,
},
activityId: 1,
rating: 2,
content: '너무 재밌게 게임했습니다. 다음에 또 하고 싶어요. 최곱니다!!!',
createdAt: '2024-03-25T15:24:25.173Z',
updatedAt: '2024-03-23T15:24:25.173Z',
},
{
id: 5,
user: {
profileImageUrl: USER_DATA.profileImageUrl,
nickname: USER_DATA.nickname,
id: USER_DATA.id,
},
activityId: 1,
rating: 2,
content: '너무 재밌게 게임했습니다. 다음에 또 하고 싶어요. 최곱니다!!!',
createdAt: '2024-03-25T15:24:25.173Z',
updatedAt: '2024-03-23T15:24:25.173Z',
},
{
id: 6,
user: {
profileImageUrl: USER_DATA.profileImageUrl,
nickname: USER_DATA.nickname,
id: USER_DATA.id,
},
activityId: 1,
rating: 2,
content: '너무 재밌게 게임했습니다. 다음에 또 하고 싶어요. 최곱니다!!!',
createdAt: '2024-03-25T15:24:25.173Z',
updatedAt: '2024-03-23T15:24:25.173Z',
},
],
};
5 changes: 5 additions & 0 deletions src/constants/sortOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ export const SORT_OPTIONS = [
{ title: '최신순', value: 'desc' as Order },
{ title: '오래된순', value: 'asc' as Order },
];

export const REVIEW_SORT_OPTIONS = [
{ title: '평점 높은순', value: 'desc' as Order },
{ title: '평점 낮은순', value: 'asc' as Order },
grapefruit13 marked this conversation as resolved.
Show resolved Hide resolved
];
20 changes: 20 additions & 0 deletions src/types/myActivities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,23 @@ export type ActivityResponse = {
createdAt: string;
updatedAt: string;
};

export type ReviewResponse = {
averageRating: number;
totalCount: number;
reviews: Review[];
};

export type Review = {
id: number;
user: {
profileImageUrl: string;
nickname: string;
id: number;
};
activityId: number;
rating: number;
content: string;
createdAt: string;
updatedAt: string;
};