reviews
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user