Files
reui-blocks/blocks/reviews3.tsx
T

298 lines
10 KiB
TypeScript

"use client";
import { BadgeCheck, ThumbsUp } from "lucide-react";
import { useState } from "react";
import { Rating } from "@/components/shadcnblocks/rating";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
interface Review {
id: string;
rating: number;
title: string;
content: string;
author: {
name: string;
avatar?: string;
};
date: string;
verified?: boolean;
images?: string[];
helpful?: number;
variant?: string;
}
const DEFAULT_REVIEWS: Review[] = [
{
id: "1",
rating: 5,
title: "کیفیت فوق‌العاده",
content:
"دوخت و جزئیاتش باورنکردنی است. عکس‌ها حقش را نمی‌دهند؛ حضوری حتی بهتر به نظر می‌رسد. جنسش لوکس است و اندازه‌اش دقیقاً مناسب است. تا حالا خیلی تعریف شنیده‌ام!",
author: {
name: "سارا احمدی",
avatar: "/block-media/shadcnblocks/block/avatar-1.webp",
},
date: "۲۰ آذر ۱۴۰۳",
verified: true,
images: [
"/block-media/shadcnblocks/block/ecommerce/clothes/Person-in-Activewear-1.png",
"/block-media/shadcnblocks/block/ecommerce/clothes/Man-in-White-Hoodie-1.png",
],
helpful: 24,
variant: "سایز M، آبی نفتی",
},
{
id: "2",
rating: 4,
title: "محصول عالی، اندازه کمی کوچک",
content:
"کیفیت و طراحی را دوست دارم. کمی کوچک‌تر از اندازه معمول است؛ پیشنهاد می‌کنم یک سایز بزرگ‌تر بگیرید. به‌جز آن دقیقاً همان چیزی بود که می‌خواستم. ارسال هم سریع بود!",
author: {
name: "مازیار رضایی",
avatar: "/block-media/shadcnblocks/block/avatar-2.webp",
},
date: "۱۸ آذر ۱۴۰۳",
verified: true,
helpful: 18,
variant: "سایز L، مشکی",
},
{
id: "3",
rating: 5,
title: "علاقه جدیدم!",
content:
"ماه‌ها دنبال چنین محصولی بودم. به جزئیات با دقت زیادی توجه شده؛ از دوخت تا یراق‌آلات همه‌چیز عالی است. ارزش هر ریالش را دارد.",
author: {
name: "نیلوفر حسینی",
avatar: "/block-media/shadcnblocks/block/avatar-3.webp",
},
date: "۱۵ آذر ۱۴۰۳",
verified: true,
images: [
"/block-media/shadcnblocks/block/ecommerce/clothes/elegant-peach-scarf.png",
],
helpful: 31,
variant: "یک سایز، کرم",
},
{
id: "4",
rating: 5,
title: "فراتر از انتظار",
content:
"اول برای خرید آنلاین مردد بودم، ولی خوشحالم که سفارش دادم. رنگ دقیقاً مثل عکس است و کیفیت عالی. وقتی درباره نگهداری سوال داشتم، پشتیبانی هم سریع پاسخ داد.",
author: {
name: "امیر محمدی",
},
date: "۱۲ آذر ۱۴۰۳",
verified: false,
helpful: 12,
},
{
id: "5",
rating: 4,
title: "زیبا ولی گران",
content:
"محصول زیبا و بادوام است. مدتی به‌خاطر قیمت مردد بودم، ولی در نهایت از خریدم راضی‌ام. دوست دارم در آینده گزینه‌های رنگی بیشتری هم اضافه شود.",
author: {
name: "رؤیا کاظمی",
avatar: "/block-media/shadcnblocks/block/avatar-3.webp",
},
date: "۸ آذر ۱۴۰۳",
verified: true,
images: [
"/block-media/shadcnblocks/block/ecommerce/accessories/Elegant-Gold-Earrings-1.png",
],
helpful: 8,
variant: "طلایی",
},
];
interface Reviews3Props {
reviews?: Review[];
title?: string;
className?: string;
}
const Reviews3 = ({
reviews = DEFAULT_REVIEWS,
title = "نظرات مشتریان",
className,
}: Reviews3Props) => {
const [helpfulClicked, setHelpfulClicked] = useState<Set<string>>(new Set());
const totalReviews = reviews.length;
const averageRating =
reviews.reduce((sum, review) => sum + review.rating, 0) / totalReviews;
const formattedAverage = averageRating.toLocaleString("fa-IR", {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});
const handleHelpful = (reviewId: string) => {
setHelpfulClicked((prev) => {
const newSet = new Set(prev);
if (newSet.has(reviewId)) {
newSet.delete(reviewId);
} else {
newSet.add(reviewId);
}
return newSet;
});
};
const getHelpfulCount = (review: Review) => {
const base = review.helpful ?? 0;
const total = base + (helpfulClicked.has(review.id) ? 1 : 0);
return total.toLocaleString("fa-IR");
};
return (
<section
dir="rtl"
lang="fa"
className={cn("py-16 md:py-24", className)}
>
<div className="container max-w-3xl">
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 className="text-xl font-semibold tracking-tight md:text-3xl">
{title}
</h2>
<div className="mt-2 flex items-center gap-2">
<Rating rate={averageRating} className="[&_svg]:size-5" />
<span className="text-lg font-semibold">{formattedAverage}</span>
<span className="text-sm text-muted-foreground">
({totalReviews.toLocaleString("fa-IR")} نظر)
</span>
</div>
</div>
<Button variant="outline">ثبت نظر</Button>
</div>
<div className="space-y-0">
{reviews.map((review, index) => (
<div key={review.id}>
{index > 0 && <Separator className="my-6" />}
<Card className="border-0 p-0 shadow-none">
<CardContent className="space-y-4 p-0">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<Avatar className="size-10">
<AvatarImage
src={review.author.avatar}
alt={review.author.name}
/>
<AvatarFallback>
{review.author.name
.split(/\s+/)
.slice(0, 2)
.map((part) => part[0])
.join("")}
</AvatarFallback>
</Avatar>
<div>
<div className="flex items-center gap-2">
<span className="font-medium">
{review.author.name}
</span>
{review.verified && (
<span
className="flex items-center gap-1 text-emerald-600"
title="خرید تأییدشده"
>
<BadgeCheck className="size-4" />
<span className="sr-only">خرید تأییدشده</span>
</span>
)}
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>{review.date}</span>
{review.variant && (
<>
<span>·</span>
<span>{review.variant}</span>
</>
)}
</div>
</div>
</div>
<Rating
rate={review.rating}
className="[&_svg]:size-4 [&>div]:size-4"
/>
</div>
<div>
<h3 className="font-medium">{review.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
{review.content}
</p>
</div>
{review.images && review.images.length > 0 && (
<div className="flex gap-2">
{review.images.map((image, imgIndex) => (
<div
key={imgIndex}
className="size-20 overflow-hidden rounded-lg sm:size-24"
>
<AspectRatio ratio={1}>
<img
src={image}
alt={`تصویر نظر ${(imgIndex + 1).toLocaleString("fa-IR")}`}
className="size-full object-cover"
/>
</AspectRatio>
</div>
))}
</div>
)}
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
className={cn(
"h-8 gap-1.5 text-muted-foreground",
helpfulClicked.has(review.id) && "text-foreground",
)}
onClick={() => handleHelpful(review.id)}
>
<ThumbsUp
className={cn(
"size-4",
helpfulClicked.has(review.id) && "fill-current",
)}
/>
مفید بود
{review.helpful !== undefined && (
<span>({getHelpfulCount(review)})</span>
)}
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 text-muted-foreground"
>
گزارش
</Button>
</div>
</CardContent>
</Card>
</div>
))}
</div>
</div>
</section>
);
};
export { Reviews3 };