This commit is contained in:
hamid zarghami
2025-10-29 16:38:38 +03:30
parent 816b172704
commit 1b53833159
5 changed files with 90 additions and 27 deletions
+71 -23
View File
@@ -3,33 +3,42 @@ import { FC, useMemo, useState } from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Star1 } from 'iconsax-react' import { Star1 } from 'iconsax-react'
import { Product, Review } from '@/types/product.types' import { Product } from '@/types/product.types'
import AddComment from './AddComment' import AddComment from './AddComment'
import { useGetProductComments } from '../hooks/useProductData'
import PageLoading from '@/components/PageLoading'
import moment from 'moment-jalaali'
interface ReviewsProps { interface ReviewsProps {
product: Product 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 [sort, setSort] = useState<'new' | 'helpful'>('new')
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const sortedReviews = useMemo(() => {
const comments = useMemo(() => commentsData?.results?.comments || [], [commentsData?.results?.comments])
const sortedComments = useMemo(() => {
if (sort === 'new') { 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) return [...comments].sort((a, b) => (b.status === 'approved' ? 1 : 0) - (a.status === 'approved' ? 1 : 0))
}, [sort, reviews]) }, [sort, comments])
const approvedComments = useMemo(() => {
return comments.filter(comment => comment.status === 'approved')
}, [comments])
const average = useMemo(() => { const average = useMemo(() => {
if (reviews.length === 0) return '0.0' if (approvedComments.length === 0) return '0.0'
const sum = reviews.reduce((acc, r) => acc + r.rating, 0) const sum = approvedComments.reduce((acc, comment) => acc + comment.rate, 0)
return (sum / reviews.length).toFixed(1) return (sum / approvedComments.length).toFixed(1)
}, [reviews]) }, [approvedComments])
const formatDate = (dateString: string) => { if (isLoading) {
const date = new Date(dateString) return <PageLoading />
return date.toLocaleDateString('fa-IR')
} }
return ( return (
@@ -66,29 +75,67 @@ const Reviews: FC<ReviewsProps> = ({ reviews = [] }) => {
</div> </div>
<div className="mt-4 sm:mt-6 rounded-xl bg-white"> <div className="mt-4 sm:mt-6 rounded-xl bg-white">
{sortedReviews.length > 0 ? ( {sortedComments.length > 0 ? (
sortedReviews.map((review, idx) => ( sortedComments.map((comment, idx) => (
<div key={review._id} className="px-4 sm:px-6 py-4 sm:py-5"> <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="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"> <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>
<div className="flex flex-wrap items-center gap-1 sm:gap-2 text-xs text-[#999999]"> <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="px-2 py-0.5 rounded bg-[#F1F1F1] text-[#7F7F7F]">خریدار</span>
)} )}
<span className="size-1.5 rounded-full bg-[#E5E5E5]"></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 className="size-1.5 rounded-full bg-[#E5E5E5]"></span>
<span>{review.user.name}</span> <span>{comment.user.fullName}</span>
</div> </div>
</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"> <div className="mt-3 text-[#333333] text-xs sm:text-sm leading-6 sm:leading-7 text-right">
{review.comment} {comment.content}
</div> </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> </div>
)) ))
) : ( ) : (
@@ -125,3 +172,4 @@ const Reviews: FC<ReviewsProps> = ({ reviews = [] }) => {
export default Reviews export default Reviews
+1 -1
View File
@@ -75,7 +75,7 @@ const SingleProduct = ({ id }: SingleProductProps) => {
<Specifications product={product} /> <Specifications product={product} />
<Description product={product} /> <Description product={product} />
<ProsCons product={product} /> <ProsCons product={product} />
<Reviews product={product} reviews={reviews} /> <Reviews product={product} />
<Questions product={product} questions={questions} /> <Questions product={product} questions={questions} />
</div> </div>
</div> </div>
+8
View File
@@ -56,3 +56,11 @@ export const useSaveReport = () => {
mutationFn: api.saveReport, mutationFn: api.saveReport,
}); });
}; };
export const useGetProductComments = (productId: number) => {
return useQuery({
queryKey: ["product-comments", productId],
queryFn: () => api.getProductComments(productId),
enabled: !!productId,
});
};
+8
View File
@@ -9,6 +9,7 @@ import {
ProductQuestionsResponse, ProductQuestionsResponse,
SaveReportTypes, SaveReportTypes,
} from "../types/Types"; } from "../types/Types";
import { CommentsResponse } from "@/app/profile/types/ProfileTypes";
export const getProduct = async ( export const getProduct = async (
id: string id: string
@@ -74,3 +75,10 @@ export const saveReport = async (params: SaveReportTypes) => {
); );
return data; return data;
}; };
export const getProductComments = async (
productId: number
): Promise<CommentsResponse> => {
const { data } = await axios.get(`/product/${productId}/comments`);
return data;
};
+2 -3
View File
@@ -225,7 +225,7 @@ export interface CommentCategory {
theme: string; theme: string;
description: string; description: string;
leaf: boolean; leaf: boolean;
parent: string; parent: string | null;
} }
export interface CommentSpecification { export interface CommentSpecification {
@@ -254,10 +254,9 @@ export interface CommentProduct {
imagesUrl: CommentImagesUrl; imagesUrl: CommentImagesUrl;
isFake: string; isFake: string;
voice: string | null; voice: string | null;
specifications: CommentSpecification[]; specifications: unknown[];
brand: CommentBrand; brand: CommentBrand;
category: CommentCategory; category: CommentCategory;
createdAt: string;
} }
export interface Comment { export interface Comment {