diff --git a/src/components/common/Header.jsx b/src/components/common/Header.jsx index cf4819e..9810588 100644 --- a/src/components/common/Header.jsx +++ b/src/components/common/Header.jsx @@ -4,9 +4,10 @@ import { Link } from 'react-router-dom'; import styled from '@emotion/styled'; -import Responsive from '../../styles/Responsive'; -import palette from '../../styles/palette'; import Button from '../../styles/Button'; +import palette from '../../styles/palette'; +import Responsive from '../../styles/Responsive'; + import { LOGOUT, LOGIN, REGISTER } from '../../util/constants/constants'; const HeaderWrapper = styled.div` diff --git a/src/components/introduce/AverageReview.jsx b/src/components/introduce/AverageReview.jsx new file mode 100644 index 0000000..426b6c2 --- /dev/null +++ b/src/components/introduce/AverageReview.jsx @@ -0,0 +1,93 @@ +import React from 'react'; + +import StarRatings from 'react-star-ratings'; + +import styled from '@emotion/styled'; + +import palette from '../../styles/palette'; + +const AverageReviewWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 1.5rem 0 1rem 0; + border: 1px solid ${palette.gray[3]}; + border-radius: 5px; +`; + +const AverageReviewTitle = styled.div` + line-height: 40px; + color: ${palette.gray[7]}; + font-size: 1.6rem; + font-weight: bold; + + span { + font-size: 1.8rem; + color: ${palette.teal[5]}; + } +`; + +const AverageRatingWrapper = styled.div` + line-height: 50px; + + em { + margin-left: .5rem; + } + + .average-rating { + font-size: 2rem; + font-weight: bold; + color: ${palette.gray[7]}; + } + + .total-rating { + font-size: 1.5rem; + font-weight: bold; + color: ${palette.gray[5]}; + } +`; + +const averageReviews = (reviews) => (reviews + .reduce((acc, { rating }) => acc + rating, 0) / reviews.length).toFixed(1) * 2; + +const convertToRating = (rating) => { + if (Number.isInteger(rating)) { + return `${rating}.0`; + } + + return rating; +}; + +const AverageReview = ({ reviews }) => { + const averageRating = averageReviews(reviews); + + return ( + + + 스터디를 참여한  + + {reviews.length} + + 명의 회원 평균평점 + + + + + {convertToRating(averageRating)} + + + / 10.0 + + + + ); +}; + +export default AverageReview; diff --git a/src/components/introduce/AverageReview.test.jsx b/src/components/introduce/AverageReview.test.jsx new file mode 100644 index 0000000..73237c9 --- /dev/null +++ b/src/components/introduce/AverageReview.test.jsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import { render } from '@testing-library/react'; + +import AverageReview from './AverageReview'; + +describe('AverageReview', () => { + const renderAverageReview = (reviews) => render(( + + )); + + context('When the average rating is integer', () => { + const reviews = [ + { rating: 3 }, + { rating: 3 }, + ]; + it('Renders average reviews contents', () => { + const { container } = renderAverageReview(reviews); + + expect(container).toHaveTextContent(`스터디를 참여한 ${reviews.length}명의 회원 평균평점`); + expect(container).toHaveTextContent(6.0); + }); + }); + + context("When the average rating isn't integer", () => { + const reviews = [ + { rating: 3 }, + { rating: 4 }, + { rating: 4 }, + ]; + it('Renders average reviews contents', () => { + const { container } = renderAverageReview(reviews); + + expect(container).toHaveTextContent(`스터디를 참여한 ${reviews.length}명의 회원 평균평점`); + expect(container).toHaveTextContent(7.4); + }); + }); +}); diff --git a/src/components/introduce/IntroduceForm.jsx b/src/components/introduce/IntroduceForm.jsx index fff85ab..236447f 100644 --- a/src/components/introduce/IntroduceForm.jsx +++ b/src/components/introduce/IntroduceForm.jsx @@ -8,8 +8,10 @@ import styled from '@emotion/styled'; import { INTRODUCE_FORM_TITLE } from '../../util/constants/constants'; import { authorizedUsersNumber, changeDateToTime } from '../../util/utils'; -import Tags from '../common/Tags'; import palette from '../../styles/palette'; +import SubTitle from '../../styles/SubTitle'; + +import Tags from '../common/Tags'; import DateTimeChange from '../common/DateTimeChange'; import IntroduceActionButtons from './IntroduceActionButtons'; @@ -20,6 +22,10 @@ const mq = facepaint([ '@media(min-width: 1150px)', ]); +const IntroduceFormWrapper = styled.div` + margin-bottom: 3rem; +`; + const IntroduceReferenceWrapper = styled.div` ${mq({ @@ -69,17 +75,6 @@ ${mq({ border-right: 0.1rem solid ${palette.gray[3]}; `; -const IntroduceContentTitle = styled.div` - font-size: 1.4rem; - font-weight: bold; - text-align: center; - margin-bottom: 0; - margin-top: 1rem; - padding: 7px 2rem 7px 2rem; - border-bottom: 2px solid ${palette.violet[3]}; - width: 17%; -`; - const IntroduceContent = styled.div` position: relative; margin-top: 2rem; @@ -104,7 +99,7 @@ const IntroduceForm = ({ const isCheckOwnGroupPost = user && (user === moderatorId); return ( - <> +
{`🙋‍♂️${moderatorId}`} @@ -128,9 +123,7 @@ const IntroduceForm = ({ page="introduce" /> - - {INTRODUCE} - + @@ -141,7 +134,7 @@ const IntroduceForm = ({ /> )} - + ); }; diff --git a/src/components/introduce/Review.jsx b/src/components/introduce/Review.jsx index ffdff1e..12e2e30 100644 --- a/src/components/introduce/Review.jsx +++ b/src/components/introduce/Review.jsx @@ -1,9 +1,36 @@ import React from 'react'; +import Moment from 'react-moment'; + +import StarRatings from 'react-star-ratings'; + import styled from '@emotion/styled'; +import palette from '../../styles/palette'; + +import { changeDateToTime } from '../../util/utils'; + const ReviewWrapper = styled.div` + background-color: #f8f8f8; + display: flex; + flex-direction: column; + margin: 1rem 0 1rem 0; + padding: 20px 35px 20px 35px; + border: 1px solid ${palette.gray[3]}; + border-radius: 5px; +`; + +const ReviewContent = styled.div` + font-size: 1.1rem; + color: ${palette.gray[8]}; + margin: .7rem 0 .8rem 0; +`; +const ReviewContentInfo = styled.div` + font-size: 0.9rem; + color: ${palette.gray[5]}; + display: flex; + justify-content: space-between; `; const Review = ({ review }) => { @@ -13,15 +40,29 @@ const Review = ({ review }) => { return ( -
- {rating} -
-
+ + {content} -
-
- {`${id} | ${createDate}`} -
+ + +
+ {id} +
+ + {changeDateToTime(createDate)} + +
); }; diff --git a/src/components/introduce/Review.test.jsx b/src/components/introduce/Review.test.jsx index c98ea3c..93926b3 100644 --- a/src/components/introduce/Review.test.jsx +++ b/src/components/introduce/Review.test.jsx @@ -22,7 +22,6 @@ describe('Review', () => { const { container } = renderReview(mockReview); expect(container).toHaveTextContent('review'); - expect(container).toHaveTextContent(3); expect(container).toHaveTextContent('test@test.com'); }); }); diff --git a/src/components/introduce/ReviewForm.jsx b/src/components/introduce/ReviewForm.jsx index 76c4f57..e0f13fe 100644 --- a/src/components/introduce/ReviewForm.jsx +++ b/src/components/introduce/ReviewForm.jsx @@ -21,7 +21,7 @@ const mq = facepaint([ const StudyReviewFormWrapper = styled.div` display: flex; flex-direction: column; - margin: 3rem 0 3rem 0; + margin: 2rem 0 3rem 0; padding: 20px 20px 20px 20px; border: 1px solid ${palette.gray[3]}; border-radius: 5px; diff --git a/src/components/introduce/ReviewList.jsx b/src/components/introduce/ReviewList.jsx index 379405a..720baef 100644 --- a/src/components/introduce/ReviewList.jsx +++ b/src/components/introduce/ReviewList.jsx @@ -2,24 +2,52 @@ import React from 'react'; import _ from 'lodash'; +import styled from '@emotion/styled'; + +import palette from '../../styles/palette'; + import Review from './Review'; +import AverageReview from './AverageReview'; + +const ReviewWrapper = styled.div` + margin: 2rem 0 3rem 0; +`; + +const EmptyReviewWrapper = styled.div` + background-color: #f8f8f8; + font-size: 1.1rem; + font-weight: bold; + color: ${palette.gray[6]}; + display: flex; + align-items: center; + flex-direction: column; + margin: 2rem 0 3rem 0; + padding: 45px; + border: 1px solid ${palette.gray[3]}; + border-radius: 5px; +`; const ReviewList = ({ reviews }) => { if (_.isEmpty(reviews)) { return ( -
아직 리뷰가 존재하지 않습니다!
+ + 등록된 리뷰가 존재하지 않습니다! + ); } return ( - <> + + {reviews.map((review) => ( ))} - + ); }; diff --git a/src/components/introduce/ReviewList.test.jsx b/src/components/introduce/ReviewList.test.jsx index 3c8ce7f..346624e 100644 --- a/src/components/introduce/ReviewList.test.jsx +++ b/src/components/introduce/ReviewList.test.jsx @@ -22,9 +22,10 @@ describe('ReviewList', () => { it('Render reviews', () => { const { container } = renderReviewList(mockReviews); + expect(container).toHaveTextContent('스터디를 참여한 1명의 회원 평균평점'); + expect(container).toHaveTextContent('6.0'); expect(container).toHaveTextContent('review'); expect(container).toHaveTextContent('test@test.com'); - expect(container).toHaveTextContent(3); }); }); @@ -32,7 +33,7 @@ describe('ReviewList', () => { it('Render nothing review message', () => { const { container } = renderReviewList([]); - expect(container).toHaveTextContent('아직 리뷰가 존재하지 않습니다!'); + expect(container).toHaveTextContent('등록된 리뷰가 존재하지 않습니다!'); }); }); }); diff --git a/src/containers/introduce/ReviewContainer.jsx b/src/containers/introduce/ReviewContainer.jsx index dac6b53..2d9ad34 100644 --- a/src/containers/introduce/ReviewContainer.jsx +++ b/src/containers/introduce/ReviewContainer.jsx @@ -8,6 +8,8 @@ import { } from '../../util/utils'; import { changeStudyReviewFields, setStudyReview } from '../../reducers/groupSlice'; +import SubTitle from '../../styles/SubTitle'; + import ReviewForm from '../../components/introduce/ReviewForm'; import ReviewList from '../../components/introduce/ReviewList'; @@ -37,9 +39,7 @@ const ReviewFormContainer = () => { return null; } - const { - participants, personnel, applyEndDate, - } = group; + const { participants, personnel, applyEndDate } = group; const isApplyTime = isCheckedTimeStatus({ applyEndTime: changeDateToTime(applyEndDate), @@ -54,6 +54,7 @@ const ReviewFormContainer = () => { return ( <> + { const { container } = renderReviewContainer(); - expect(container).toHaveTextContent('아직 리뷰가 존재하지 않습니다!'); + expect(container).toHaveTextContent('등록된 리뷰가 존재하지 않습니다!'); }); context('with login and group', () => { diff --git a/src/reducers/groupSlice.js b/src/reducers/groupSlice.js index 03ac9d1..934c997 100644 --- a/src/reducers/groupSlice.js +++ b/src/reducers/groupSlice.js @@ -137,6 +137,39 @@ const { actions, reducer } = createSlice({ studyReviewFields: studyReviewInitialState, }; }, + + setGroupReview(state, { payload: review }) { + const { group } = state; + + if (group.reviews) { + return { + ...state, + group: { + ...group, + reviews: [ + { + ...review, + createDate: new Date().toString(), + }, + ...group.reviews, + ], + }, + }; + } + + return { + ...state, + group: { + ...group, + reviews: [ + { + ...review, + createDate: new Date().toString(), + }, + ], + }, + }; + }, }, }); @@ -152,6 +185,7 @@ export const { setOriginalArticle, changeStudyReviewFields, clearStudyReviewFields, + setGroupReview, } = actions; export const loadStudyGroups = (tag) => async (dispatch) => { @@ -298,6 +332,7 @@ export const setStudyReview = (review) => async (dispatch, getState) => { review, }); + dispatch(setGroupReview(review)); dispatch(clearStudyReviewFields()); }; diff --git a/src/reducers/groupSlice.test.js b/src/reducers/groupSlice.test.js index 043c8fc..4aac8b3 100644 --- a/src/reducers/groupSlice.test.js +++ b/src/reducers/groupSlice.test.js @@ -24,6 +24,7 @@ import reducer, { changeStudyReviewFields, clearStudyReviewFields, setStudyReview, + setGroupReview, } from './groupSlice'; import STUDY_GROUPS from '../../fixtures/study-groups'; @@ -268,6 +269,46 @@ describe('reducer', () => { expect(content).toBe(''); }); }); + + describe('setGroupReview', () => { + const review = { + id: 'test', + content: 'test', + rating: 3, + }; + + context('When the group reviews field is exists', () => { + const initialState = { + group: { + reviews: [], + }, + }; + + it('Set in the group review field', () => { + const state = reducer(initialState, setGroupReview(review)); + + const { group: { reviews } } = state; + + expect(reviews[0].id).toBe('test'); + expect(reviews[0].rating).toBe(3); + }); + }); + + context("When the group reviews field isn't exists", () => { + const initialState = { + group: {}, + }; + + it('Set in the group review field', () => { + const state = reducer(initialState, setGroupReview(review)); + + const { group: { reviews } } = state; + + expect(reviews[0].id).toBe('test'); + expect(reviews[0].rating).toBe(3); + }); + }); + }); }); describe('async actions', () => { @@ -592,9 +633,9 @@ describe('async actions', () => { }); }); - it('dispatches clearStudyReviewFields', async () => { + it('dispatches setGroupReview and clearStudyReviewFields', async () => { await store.dispatch(setStudyReview({ - user: 'user', + id: 'user', review: 'test', rating: 5, })); @@ -602,6 +643,14 @@ describe('async actions', () => { const actions = store.getActions(); expect(actions[0]).toEqual({ + type: 'group/setGroupReview', + payload: { + rating: 5, + review: 'test', + id: 'user', + }, + }); + expect(actions[1]).toEqual({ type: 'group/clearStudyReviewFields', }); }); diff --git a/src/styles/SubTitle.jsx b/src/styles/SubTitle.jsx new file mode 100644 index 0000000..22e515b --- /dev/null +++ b/src/styles/SubTitle.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import styled from '@emotion/styled'; + +import palette from './palette'; + +const SubTitleWrapper = styled.div` + font-size: 1.4rem; + font-weight: bold; + text-align: center; + margin-bottom: 0; + margin-top: 1rem; + padding: 7px 2rem 7px 2rem; + border-bottom: 2px solid ${palette.violet[3]}; + width: 17%; +`; + +const SubTitle = ({ title }) => ( + + {title} + +); + +export default SubTitle;