diff --git a/src/components/program/ProgramCard.tsx b/src/components/program/ProgramCard.tsx index e6bb204d7..97bb59f6e 100644 --- a/src/components/program/ProgramCard.tsx +++ b/src/components/program/ProgramCard.tsx @@ -2,7 +2,7 @@ import { Box, Icon, Text } from '@chakra-ui/react' import { MultiLineTruncationMixin } from 'lodestar-app-element/src/components/common' import PriceLabel from 'lodestar-app-element/src/components/labels/PriceLabel' import { useApp } from 'lodestar-app-element/src/contexts/AppContext' -import { useAuth } from 'lodestar-app-element/src/contexts/AuthContext' +import { useAdaptedReviewable } from 'lodestar-app-element/src/hooks/review' import React from 'react' import { AiOutlineClockCircle, AiOutlineUser } from 'react-icons/ai' import { useIntl } from 'react-intl' @@ -10,14 +10,14 @@ import { Link, useHistory } from 'react-router-dom' import styled, { css } from 'styled-components' import { durationFormatter } from '../../helpers' import { useProgramEnrollmentAggregate } from '../../hooks/program' -import { useProductEditorIds, useReviewAggregate } from '../../hooks/review' +import { useReviewAggregate } from '../../hooks/review' import EmptyCover from '../../images/empty-cover.png' import { ReactComponent as StarIcon } from '../../images/star-current-color.svg' import { Category } from '../../types/general' import { ProgramBriefProps, ProgramPlan, ProgramRole } from '../../types/program' import { CustomRatioImage } from '../common/Image' import MemberAvatar from '../common/MemberAvatar' -import StarRating from '../common/StarRating' +import ReviewScoreStarRow from '../review/ReviewScoreStarRow' import programMessages from './translation' const InstructorPlaceHolder = styled.div` @@ -150,8 +150,6 @@ const PrimaryCard: React.VFC = ({ programLink, }) => { const { formatMessage } = useIntl() - const { currentMemberId, currentUserRole } = useAuth() - const { productEditorIds } = useProductEditorIds(program.id) const { enabledModules, settings } = useApp() const history = useHistory() @@ -165,7 +163,10 @@ const PrimaryCard: React.VFC = ({ : undefined const periodAmount = program.plans.length > 1 ? program.plans[0]?.periodAmount : null const periodType = program.plans.length > 1 ? program.plans[0]?.periodType : null - const { averageScore, reviewCount } = useReviewAggregate(`/programs/${program.id}`) + + const { id: appId } = useApp() + const path = `/programs/${program.id}` + const { data: reviewable, loading: reviewableLoading } = useAdaptedReviewable(path, appId) const { data: enrolledCount } = useProgramEnrollmentAggregate(program.id, { skip: !program.isEnrolledCountVisible }) const programAdditionalSoldHeadcountSetting = settings['program.additional.sold.headcount'] || '[]' @@ -197,6 +198,8 @@ const PrimaryCard: React.VFC = ({ textColor: '#585858', } + if (reviewableLoading) return <> + return ( <> {!noInstructor && instructorId && ( @@ -245,20 +248,7 @@ const PrimaryCard: React.VFC = ({ - {enabledModules.customer_review ? ( - currentUserRole === 'app-owner' || - (currentMemberId && productEditorIds.includes(currentMemberId)) || - reviewCount >= (settings.review_lower_bound ? Number(settings.review_lower_bound) : 3) ? ( - - - ({formatMessage(programMessages.ProgramCard.reviewCount, { count: reviewCount })}) - - ) : ( - - {formatMessage(programMessages.ProgramCard.noReviews)} - - ) - ) : null} + {renderCustomDescription && renderCustomDescription()} {program.abstract} diff --git a/src/components/review/ReviewCollectionBlock.tsx b/src/components/review/ReviewCollectionBlock.tsx index db42998b1..99654536c 100644 --- a/src/components/review/ReviewCollectionBlock.tsx +++ b/src/components/review/ReviewCollectionBlock.tsx @@ -3,6 +3,7 @@ import { Divider, Icon } from '@chakra-ui/react' import { Skeleton } from 'antd' import { useApp } from 'lodestar-app-element/src/contexts/AppContext' import { useAuth } from 'lodestar-app-element/src/contexts/AuthContext' +import { useAdaptedReviewable } from 'lodestar-app-element/src/hooks/review' import React, { useRef } from 'react' import { useIntl } from 'react-intl' import styled from 'styled-components' @@ -10,12 +11,12 @@ import hasura from '../../hasura' import { reviewMessages } from '../../helpers/translation' import { useProductEditorIds, useReviewAggregate } from '../../hooks/review' import { ReactComponent as StarEmptyIcon } from '../../images/star-empty.svg' -import { ReactComponent as StarIcon } from '../../images/star.svg' import { MemberReviewProps } from '../../types/review' import ReviewAdminItemCollection from './ReviewAdminItemCollection' import ReviewMemberItemCollection, { ReviewMemberItemRef } from './ReviewMemberItemCollection' import ReviewModal from './ReviewModal' import ReviewPublicItemCollection from './ReviewPublicItemCollection' +import ReviewScorePanel from './ReviewScorePanel' const Wrapper = styled.div` div { @@ -32,16 +33,7 @@ export const StyledTitle = styled.h2` letter-spacing: 0.2px; font-weight: bold; ` -export const StyledAvgScore = styled.div` - font-weight: bold; - font-size: 40px; - letter-spacing: 1px; -` -export const StyledReviewAmount = styled.div` - color: #9b9b9b; - font-size: 14px; - letter-spacing: 0.4px; -` + const StyledEmptyText = styled.div` color: #9b9b9b; font-size: 14px; @@ -67,6 +59,7 @@ const ReviewCollectionBlock: React.VFC<{ const { formatMessage } = useIntl() const { currentMemberId, currentUserRole } = useAuth() const { settings, id: appId } = useApp() + const { data: reviewable, loading: reviewableLoading } = useAdaptedReviewable(path, appId) const { loadingReviewAggregate, averageScore, reviewCount, refetchReviewAggregate } = useReviewAggregate(path) const { loadingIsCurrentMemberEnrollment, isCurrentMemberEnrollment } = useIsCurrentMemberEnrollment( targetId, @@ -91,6 +84,7 @@ const ReviewCollectionBlock: React.VFC<{ ) if ( + reviewableLoading || loadingIsCurrentMemberEnrollment || loadingProductEditorIds || loadingReviewAggregate || @@ -105,23 +99,18 @@ const ReviewCollectionBlock: React.VFC<{ ) } - return ( + return !isProductAdmin && + !reviewable?.is_score_viewable && + !reviewable?.is_item_viewable && + !reviewable?.is_writable ? ( + <> + ) : ( <> {title || formatMessage(reviewMessages.title.programReview)}
- - {isMoreThanReviewLowerBound || isProductAdmin ? (averageScore === 0 ? 0 : averageScore?.toFixed(1)) : 0} - -
- -
- - {formatMessage(reviewMessages.text.reviewAmount, { - amount: isMoreThanReviewLowerBound || isProductAdmin ? reviewCount : 0, - })} - + {isMoreThanReviewLowerBound || isProductAdmin ? : <>} {isCurrentMemberEnrollment ? ( ({ - onRefetchReviewMemberItem: () => onRefetch(), + onRefetchReviewMemberItem: () => { + onRefetch() + onRefetchCurrentMemberReviews() + }, })) - if (loadingReviews) { + if (loadingCurrentMemberReviews && loadingReviews && reviewableLoading) { return ( @@ -46,7 +61,7 @@ const ReviewMemberItemCollection: React.ForwardRefRenderFunction< return ( <>
- {memberReviews.map(v => ( + {currentMemberReviews.concat(memberReviews).map(v => (
{ +const useReviewMemberCollection = ( + path: string, + appId: string, + currentMemberId: string | null, + isItemViewable: boolean = true, + isBelongedToCurrentMember: boolean = false, +) => { const condition: hasura.GET_REVIEW_MEMBERVariables['condition'] = { path: { _eq: path }, app_id: { _eq: appId }, + member_id: isBelongedToCurrentMember + ? { _eq: currentMemberId } + : isItemViewable + ? { _neq: currentMemberId } + : { _eq: '' }, } const { loading, error, data, refetch, fetchMore } = useQuery< diff --git a/src/components/review/ReviewModal.tsx b/src/components/review/ReviewModal.tsx index dc496049d..52d1bda90 100644 --- a/src/components/review/ReviewModal.tsx +++ b/src/components/review/ReviewModal.tsx @@ -13,6 +13,7 @@ import { import BraftEditor, { EditorState } from 'braft-editor' import { useApp } from 'lodestar-app-element/src/contexts/AppContext' import { useAuth } from 'lodestar-app-element/src/contexts/AuthContext' +import { useAdaptedReviewable } from 'lodestar-app-element/src/hooks/review' import React, { useState } from 'react' import { Controller, useForm } from 'react-hook-form' import { AiOutlineEdit as EditIcon } from 'react-icons/ai' @@ -125,6 +126,8 @@ const ReviewModal: React.VFC<{ }, }) + const { data: reviewable, loading: reviewableLoading } = useAdaptedReviewable(path, appId) + const validateTitle = (value: string) => !!value || formatMessage(reviewMessages.validate.titleIsRequired) const handleSave = handleSubmit(({ starRating, title, content, privateContent }) => { @@ -169,47 +172,50 @@ const ReviewModal: React.VFC<{ onClose() }) } else { - insertReview({ - variables: { - path, - memberId: currentMemberId, - score: starRating, - title, - content: content.toRAW(), - privateContent: privateContent.toRAW(), - appId: appId, - }, - }) - .then(() => { - toast({ - title: formatMessage(commonMessages.event.successfullySaved), - status: 'success', - duration: 3000, - isClosable: false, - position: 'top', - }) - reset() - onRefetchReviewMemberItem?.() - onRefetchReviewAggregate?.() - window.location.replace(`/programs/${programId}?moveToBlock=customer-review&visitIntro=1`) - }) - .catch(error => process.env.NODE_ENV === 'development' && console.error(error)) - .finally(() => { - setIsSubmitting(false) - onClose() + reviewable.is_item_viewable && + insertReview({ + variables: { + path, + memberId: currentMemberId, + score: starRating, + title, + content: content.toRAW(), + privateContent: privateContent.toRAW(), + appId: appId, + }, }) + .then(() => { + toast({ + title: formatMessage(commonMessages.event.successfullySaved), + status: 'success', + duration: 3000, + isClosable: false, + position: 'top', + }) + reset() + onRefetchReviewMemberItem?.() + onRefetchReviewAggregate?.() + window.location.replace(`/programs/${programId}?moveToBlock=customer-review&visitIntro=1`) + }) + .catch(error => process.env.NODE_ENV === 'development' && console.error(error)) + .finally(() => { + setIsSubmitting(false) + onClose() + }) } }) + if (reviewableLoading) return <> + return ( <> - {authToken && ( + {authToken && (reviewable?.is_writable || memberReviews?.length > 0) && ( - {memberReviews && memberReviews.length !== 0 + {memberReviews?.length > 0 ? formatMessage(reviewMessages.button.editReview) : formatMessage(reviewMessages.button.toReview)} diff --git a/src/components/review/ReviewPublicItemCollection.tsx b/src/components/review/ReviewPublicItemCollection.tsx index 8d7fe11a8..2db5666c5 100644 --- a/src/components/review/ReviewPublicItemCollection.tsx +++ b/src/components/review/ReviewPublicItemCollection.tsx @@ -1,5 +1,6 @@ import { gql, useQuery } from '@apollo/client' import { Box, Button, SkeletonCircle, SkeletonText } from '@chakra-ui/react' +import { useAdaptedReviewable } from 'lodestar-app-element/src/hooks/review' import React, { useState } from 'react' import { useIntl } from 'react-intl' import hasura from '../../hasura' @@ -17,8 +18,9 @@ const ReviewPublicItemCollection: React.VFC<{ const [loading, setLoading] = useState(false) const { loadingReviews, publicReviews, loadMoreReviews } = useReviewPublicCollection(path, appId) + const { data: reviewable, loading: reviewableLoading } = useAdaptedReviewable(path, appId) - if (loadingReviews) { + if (loadingReviews && reviewableLoading) { return ( @@ -27,7 +29,9 @@ const ReviewPublicItemCollection: React.VFC<{ ) } - return ( + return !reviewable?.is_item_viewable ? ( + <> + ) : ( <>
{publicReviews.map(v => ( diff --git a/src/components/review/ReviewScorePanel.tsx b/src/components/review/ReviewScorePanel.tsx new file mode 100644 index 000000000..d5f19f605 --- /dev/null +++ b/src/components/review/ReviewScorePanel.tsx @@ -0,0 +1,45 @@ +import Icon from '@chakra-ui/icon' +import { useAdaptedReviewable } from 'lodestar-app-element/src/hooks/review' +import { FC } from 'react' +import { useIntl } from 'react-intl' +import styled from 'styled-components' +import { reviewMessages } from '../../helpers/translation' +import { useReviewAggregate } from '../../hooks/review' +import { ReactComponent as StarIcon } from '../../images/star.svg' + +export const StyledAvgScore = styled.div` + font-weight: bold; + font-size: 40px; + letter-spacing: 1px; +` +export const StyledReviewAmount = styled.div` + color: #9b9b9b; + font-size: 14px; + letter-spacing: 0.4px; +` + +const ReviewScorePanel: FC<{ path: string; appId: string }> = ({ path, appId }) => { + const { formatMessage } = useIntl() + const { loadingReviewAggregate, averageScore, reviewCount } = useReviewAggregate(path) + const { data: reviewable, loading: reviewableLoading } = useAdaptedReviewable(path, appId) + + if (reviewableLoading || loadingReviewAggregate) return <> + + return !reviewable?.is_score_viewable ? ( + <> + ) : ( + <> + {averageScore === 0 ? 0 : averageScore?.toFixed(1)} +
+ +
+ + {formatMessage(reviewMessages.text.reviewAmount, { + amount: reviewCount, + })} + + + ) +} + +export default ReviewScorePanel diff --git a/src/components/review/ReviewScoreStarRow.tsx b/src/components/review/ReviewScoreStarRow.tsx new file mode 100644 index 000000000..e220326ea --- /dev/null +++ b/src/components/review/ReviewScoreStarRow.tsx @@ -0,0 +1,41 @@ +import { useApp } from 'lodestar-app-element/src/contexts/AppContext' +import { useAuth } from 'lodestar-app-element/src/contexts/AuthContext' +import { useAdaptedReviewable, useReviewAggregate } from 'lodestar-app-element/src/hooks/review' +import { FC } from 'react' +import { useIntl } from 'react-intl' +import styled from 'styled-components' +import { reviewMessages } from '../../helpers/translation' +import StarRating from '../common/StarRating' + +const StyledReviewRating = styled.div` + color: var(--gray-dark); + font-size: 14px; + letter-spacing: 0.4px; + text-align: justify; +` + +const ReviewScoreStarRow: FC<{ path: string; appId: string }> = ({ path, appId }) => { + const { formatMessage } = useIntl() + const { enabledModules, settings } = useApp() + const { currentUserRole } = useAuth() + const { data: reviewable, loading: reviewableLoading } = useAdaptedReviewable(path, appId) + const { averageScore, reviewCount, loading: reviewAggregateLoading } = useReviewAggregate(path) + + if (reviewableLoading || reviewAggregateLoading) return <> + if (path === '/programs/4759ac4a-c838-40ce-92d5-142ff0ee8c31') console.log(averageScore, reviewCount) + + return enabledModules.customer_review ? ( + currentUserRole === 'app-owner' || + (reviewable?.is_score_viewable && + reviewCount >= (settings.review_lower_bound ? Number(settings.review_lower_bound) : 3)) ? ( + + + ({formatMessage(reviewMessages.text.reviewCount, { count: reviewCount })}) + + ) : ( + {formatMessage(reviewMessages.text.noReviews)} + ) + ) : null +} + +export default ReviewScoreStarRow diff --git a/src/helpers/translation.ts b/src/helpers/translation.ts index ff6c50b7a..07770bf67 100644 --- a/src/helpers/translation.ts +++ b/src/helpers/translation.ts @@ -1651,6 +1651,8 @@ export const reviewMessages = { edited: { id: 'review.status.edited', defaultMessage: '已編輯' }, }), text: defineMessages({ + reviewCount: { id: 'review.text.reviewCount', defaultMessage: '{count} 則' }, + noReviews: { id: 'review.text.noReviews', defaultMessage: '目前無評價' }, reviewAmount: { id: 'review.text.amount', defaultMessage: '{amount} 則評價' }, notEnoughReviews: { id: 'review.text.notEnoughReviews', defaultMessage: '評價不足{amount}人無法顯示' }, reply: { id: 'review.text.reply', defaultMessage: '回覆...' }, diff --git a/src/translations/locales/en-us.json b/src/translations/locales/en-us.json index 4e9670946..05df4081b 100644 --- a/src/translations/locales/en-us.json +++ b/src/translations/locales/en-us.json @@ -1538,7 +1538,9 @@ "review.status.edited": "Edited", "review.text.amount": "{amount} is a review", "review.text.notEnoughReviews": "Not enough reviews for {amount} people to show", + "review.text.noReviews": "No reviews yet", "review.text.reply": "Reply to...", + "review.text.reviewCount": "{count} pieces", "review.text.reviewModalDescription": "Welcome to give encouragement, share your experience or make suggestions! To maintain the fairness of the evaluation, only three or more evaluations will be made public!", "review.title.bestReview": "Best Review", "review.title.privateMessage": "Private Message to Teacher", diff --git a/src/translations/locales/id.json b/src/translations/locales/id.json index b0dc298eb..d2c209701 100644 --- a/src/translations/locales/id.json +++ b/src/translations/locales/id.json @@ -1538,7 +1538,9 @@ "review.status.edited": "Telah diedit", "review.text.amount": "{amount} ulasan", "review.text.notEnoughReviews": "Tidak cukup {amount} ulasan untuk ditampilkan", + "review.text.noReviews": "Belum ada ulasan", "review.text.reply": "Balas...", + "review.text.reviewCount": "{count} ulasan", "review.text.reviewModalDescription": "Silakan beri dorongan, bagikan pengalaman atau ajukan saran! Untuk menjaga keadilan ulasan, minimal tiga ulasan diperlukan untuk dipublikasikan!", "review.title.bestReview": "Ulasan Terbaik", "review.title.privateMessage": "Pesan Pribadi untuk Pengajar", diff --git a/src/translations/locales/ja.json b/src/translations/locales/ja.json index c146dbeed..d11130a8c 100644 --- a/src/translations/locales/ja.json +++ b/src/translations/locales/ja.json @@ -1534,7 +1534,9 @@ "review.status.edited": "編集済み", "review.text.amount": "{amount} 件のレビュー", "review.text.notEnoughReviews": "レビューが{amount}件に満たないため表示できません", + "review.text.noReviews": "まだレビューがありません", "review.text.reply": "返信...", + "review.text.reviewCount": "{count} 件", "review.text.reviewModalDescription": "励ましの言葉、感想、提案をお寄せください!公正な評価を維持するため、3件以上のレビューが集まった場合にのみ公開されます!", "review.title.bestReview": "ベストレビュー", "review.title.privateMessage": "先生へのプライベートメッセージ", diff --git a/src/translations/locales/ko.json b/src/translations/locales/ko.json index 67e473e54..cdf16dbec 100644 --- a/src/translations/locales/ko.json +++ b/src/translations/locales/ko.json @@ -1534,7 +1534,9 @@ "review.status.edited": "수정됨", "review.text.amount": "{amount} 개의 리뷰", "review.text.notEnoughReviews": "리뷰가 {amount}개 미만이어서 표시할 수 없습니다", + "review.text.noReviews": "아직 리뷰가 없습니다", "review.text.reply": "답글...", + "review.text.reviewCount": "{count} 개의", "review.text.reviewModalDescription": "격려의 말씀, 경험 공유 또는 제안을 부탁드립니다! 리뷰의 공정성을 유지하기 위해 세 개 이상의 리뷰가 모이면 공개됩니다!", "review.title.bestReview": "베스트 리뷰", "review.title.privateMessage": "선생님께 드리는 비공개 메시지", diff --git a/src/translations/locales/vi.json b/src/translations/locales/vi.json index 3b0b24370..49e8a654a 100644 --- a/src/translations/locales/vi.json +++ b/src/translations/locales/vi.json @@ -1538,7 +1538,9 @@ "review.status.edited": "Đã chỉnh sửa", "review.text.amount": "{amount} đánh giá", "review.text.notEnoughReviews": "Không đủ {amount} đánh giá để hiển thị", + "review.text.noReviews": "Chưa có đánh giá nào", "review.text.reply": "Trả lời...", + "review.text.reviewCount": "{count} bài", "review.text.reviewModalDescription": "Hãy đưa ra lời động viên, chia sẻ kinh nghiệm hoặc đề xuất! Để đảm bảo tính công bằng của đánh giá, phải có ít nhất ba đánh giá thì mới được công khai!", "review.title.bestReview": "Đánh giá tốt nhất", "review.title.privateMessage": "Tin nhắn riêng gửi giáo viên", diff --git a/src/translations/locales/zh-cn.json b/src/translations/locales/zh-cn.json index abadf45e2..15d7421e2 100644 --- a/src/translations/locales/zh-cn.json +++ b/src/translations/locales/zh-cn.json @@ -1539,6 +1539,7 @@ "review.text.amount": "{amount} 条评价", "review.text.notEnoughReviews": "评价不足{amount}人,无法显示", "review.text.reply": "回复...", + "review.text.reviewCount": "{count} 条", "review.text.reviewModalDescription": "欢迎给予鼓励、分享心得或提出建议!为保持评价的公正性,累计三条以上评价才会公开哦!", "review.title.bestReview": "最佳评论", "review.title.privateMessage": "私下给老师的信息", diff --git a/src/translations/locales/zh-tw.json b/src/translations/locales/zh-tw.json index b9fe316e1..5724d1715 100644 --- a/src/translations/locales/zh-tw.json +++ b/src/translations/locales/zh-tw.json @@ -1538,7 +1538,9 @@ "review.status.edited": "已編輯", "review.text.amount": "{amount} 則評價", "review.text.notEnoughReviews": "評價不足{amount}人無法顯示", + "review.text.noReviews": "目前無評價", "review.text.reply": "回覆...", + "review.text.reviewCount": "{count} 則", "review.text.reviewModalDescription": "歡迎給予鼓勵、分享心得或是提出建議!為維護評價公正性,累計三則以上評價才會公開呦!", "review.title.bestReview": "最佳評論", "review.title.privateMessage": "私下給老師的訊息",