reviews
This commit is contained in:
@@ -3,33 +3,42 @@ import { FC, useMemo, useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Star1 } from 'iconsax-react'
|
||||
import { Product, Review } from '@/types/product.types'
|
||||
import { Product } from '@/types/product.types'
|
||||
import AddComment from './AddComment'
|
||||
import { useGetProductComments } from '../hooks/useProductData'
|
||||
import PageLoading from '@/components/PageLoading'
|
||||
import moment from 'moment-jalaali'
|
||||
|
||||
interface ReviewsProps {
|
||||
product: Product
|
||||
reviews?: Review[]
|
||||
}
|
||||
|
||||
const Reviews: FC<ReviewsProps> = ({ reviews = [] }) => {
|
||||
const Reviews: FC<ReviewsProps> = ({ product }) => {
|
||||
const { data: commentsData, isLoading } = useGetProductComments(product._id)
|
||||
const [sort, setSort] = useState<'new' | 'helpful'>('new')
|
||||
const [open, setOpen] = useState(false)
|
||||
const sortedReviews = useMemo(() => {
|
||||
|
||||
const comments = useMemo(() => commentsData?.results?.comments || [], [commentsData?.results?.comments])
|
||||
|
||||
const sortedComments = useMemo(() => {
|
||||
if (sort === 'new') {
|
||||
return [...reviews].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
return [...comments].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
}
|
||||
return [...reviews].sort((a, b) => b.helpful - a.helpful)
|
||||
}, [sort, reviews])
|
||||
return [...comments].sort((a, b) => (b.status === 'approved' ? 1 : 0) - (a.status === 'approved' ? 1 : 0))
|
||||
}, [sort, comments])
|
||||
|
||||
const approvedComments = useMemo(() => {
|
||||
return comments.filter(comment => comment.status === 'approved')
|
||||
}, [comments])
|
||||
|
||||
const average = useMemo(() => {
|
||||
if (reviews.length === 0) return '0.0'
|
||||
const sum = reviews.reduce((acc, r) => acc + r.rating, 0)
|
||||
return (sum / reviews.length).toFixed(1)
|
||||
}, [reviews])
|
||||
if (approvedComments.length === 0) return '0.0'
|
||||
const sum = approvedComments.reduce((acc, comment) => acc + comment.rate, 0)
|
||||
return (sum / approvedComments.length).toFixed(1)
|
||||
}, [approvedComments])
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('fa-IR')
|
||||
if (isLoading) {
|
||||
return <PageLoading />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -66,29 +75,67 @@ const Reviews: FC<ReviewsProps> = ({ reviews = [] }) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-4 sm:mt-6 rounded-xl bg-white">
|
||||
{sortedReviews.length > 0 ? (
|
||||
sortedReviews.map((review, idx) => (
|
||||
<div key={review._id} className="px-4 sm:px-6 py-4 sm:py-5">
|
||||
{sortedComments.length > 0 ? (
|
||||
sortedComments.map((comment, idx) => (
|
||||
<div key={comment._id} className="px-4 sm:px-6 py-4 sm:py-5">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-start gap-2 sm:gap-3">
|
||||
<div className="px-2 py-0.5 rounded-full bg-[#02A55A] text-white text-xs w-fit">
|
||||
{review.rating.toFixed(1)}
|
||||
{comment.rate.toFixed(1)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1 sm:gap-2 text-xs text-[#999999]">
|
||||
{review.isVerified && (
|
||||
{comment.status === 'approved' && (
|
||||
<span className="px-2 py-0.5 rounded bg-[#F1F1F1] text-[#7F7F7F]">خریدار</span>
|
||||
)}
|
||||
<span className="size-1.5 rounded-full bg-[#E5E5E5]"></span>
|
||||
<span>{formatDate(review.date)}</span>
|
||||
<span>{moment(comment.createdAt, 'jYYYY/jMM/jDD HH:mm:ss').format('jYYYY/jMM/jD')}</span>
|
||||
<span className="size-1.5 rounded-full bg-[#E5E5E5]"></span>
|
||||
<span>{review.user.name}</span>
|
||||
<span>{comment.user.fullName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{comment.title && (
|
||||
<div className="mt-2 text-[#333333] text-sm font-medium text-right">
|
||||
{comment.title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(comment.advantage && comment.advantage.length > 0) || (comment.disAdvantage && comment.disAdvantage.length > 0) ? (
|
||||
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||||
{comment.advantage && comment.advantage.length > 0 && (
|
||||
<div className="bg-[#F0F9F4] rounded-lg p-3">
|
||||
<div className="text-[#02A55A] text-xs sm:text-sm font-medium mb-2">نکات مثبت:</div>
|
||||
<ul className="text-xs sm:text-sm text-[#02A55A] space-y-1.5">
|
||||
{comment.advantage.map((item, index) => (
|
||||
<li key={index} className="flex items-start gap-2 text-right">
|
||||
<span className="text-[#02A55A] mt-0.5">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{comment.disAdvantage && comment.disAdvantage.length > 0 && (
|
||||
<div className="bg-[#FEF2F2] rounded-lg p-3">
|
||||
<div className="text-[#EF4444] text-xs sm:text-sm font-medium mb-2">نکات منفی:</div>
|
||||
<ul className="text-xs sm:text-sm text-[#EF4444] space-y-1.5">
|
||||
{comment.disAdvantage.map((item, index) => (
|
||||
<li key={index} className="flex items-start gap-2 text-right">
|
||||
<span className="text-[#EF4444] mt-0.5">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-3 text-[#333333] text-xs sm:text-sm leading-6 sm:leading-7 text-right">
|
||||
{review.comment}
|
||||
{comment.content}
|
||||
</div>
|
||||
|
||||
{idx !== sortedReviews.length - 1 && <div className="mt-4 sm:mt-5"> <Separator /> </div>}
|
||||
{idx !== sortedComments.length - 1 && <div className="mt-4 sm:mt-5"> <Separator /> </div>}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
@@ -125,3 +172,4 @@ const Reviews: FC<ReviewsProps> = ({ reviews = [] }) => {
|
||||
export default Reviews
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ const SingleProduct = ({ id }: SingleProductProps) => {
|
||||
<Specifications product={product} />
|
||||
<Description product={product} />
|
||||
<ProsCons product={product} />
|
||||
<Reviews product={product} reviews={reviews} />
|
||||
<Reviews product={product} />
|
||||
<Questions product={product} questions={questions} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,3 +56,11 @@ export const useSaveReport = () => {
|
||||
mutationFn: api.saveReport,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProductComments = (productId: number) => {
|
||||
return useQuery({
|
||||
queryKey: ["product-comments", productId],
|
||||
queryFn: () => api.getProductComments(productId),
|
||||
enabled: !!productId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ProductQuestionsResponse,
|
||||
SaveReportTypes,
|
||||
} from "../types/Types";
|
||||
import { CommentsResponse } from "@/app/profile/types/ProfileTypes";
|
||||
|
||||
export const getProduct = async (
|
||||
id: string
|
||||
@@ -74,3 +75,10 @@ export const saveReport = async (params: SaveReportTypes) => {
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getProductComments = async (
|
||||
productId: number
|
||||
): Promise<CommentsResponse> => {
|
||||
const { data } = await axios.get(`/product/${productId}/comments`);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -225,7 +225,7 @@ export interface CommentCategory {
|
||||
theme: string;
|
||||
description: string;
|
||||
leaf: boolean;
|
||||
parent: string;
|
||||
parent: string | null;
|
||||
}
|
||||
|
||||
export interface CommentSpecification {
|
||||
@@ -254,10 +254,9 @@ export interface CommentProduct {
|
||||
imagesUrl: CommentImagesUrl;
|
||||
isFake: string;
|
||||
voice: string | null;
|
||||
specifications: CommentSpecification[];
|
||||
specifications: unknown[];
|
||||
brand: CommentBrand;
|
||||
category: CommentCategory;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
|
||||
Reference in New Issue
Block a user