228 lines
8.1 KiB
TypeScript
228 lines
8.1 KiB
TypeScript
import { BadgeCheck } from "lucide-react";
|
||||
|
|
|
|||
|
|
import { Rating } from "@/components/shadcnblocks/rating";
|
|||
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|||
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|||
|
|
import { Progress } from "@/components/ui/progress";
|
|||
|
|
import { cn } from "@/lib/utils";
|
|||
|
|
|
|||
|
|
interface Review {
|
|||
|
|
id: string;
|
|||
|
|
rating: number;
|
|||
|
|
title: string;
|
|||
|
|
content: string;
|
|||
|
|
author: {
|
|||
|
|
name: string;
|
|||
|
|
avatar?: string;
|
|||
|
|
};
|
|||
|
|
date: string;
|
|||
|
|
verified?: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const DEFAULT_REVIEWS: Review[] = [
|
|||
|
|
{
|
|||
|
|
id: "1",
|
|||
|
|
rating: 5,
|
|||
|
|
title: "فراتر از انتظارم بود",
|
|||
|
|
content:
|
|||
|
|
"اول کمی تردید داشتم، ولی این محصول واقعاً جواب داد. کیفیت ساخت عالی است و زودتر از موعد به دستم رسید. اگر هنوز مردد هستید، حتماً پیشنهاد میکنم امتحان کنید.",
|
|||
|
|
author: {
|
|||
|
|
name: "سارا احمدی",
|
|||
|
|
avatar: "/block-media/shadcnblocks/block/avatar-1.webp",
|
|||
|
|
},
|
|||
|
|
date: "۲۰ آذر ۱۴۰۳",
|
|||
|
|
verified: true,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "2",
|
|||
|
|
rating: 4,
|
|||
|
|
title: "ارزش خرید بالا",
|
|||
|
|
content:
|
|||
|
|
"بهطور کلی محصول خوبی است و دقیقاً همان کاری را میکند که باید. یک ستاره کم کردم چون بستهبندی میتوانست بهتر باشد، ولی خود محصول عالی است.",
|
|||
|
|
author: {
|
|||
|
|
name: "مازیار رضایی",
|
|||
|
|
avatar: "/block-media/shadcnblocks/block/avatar-2.webp",
|
|||
|
|
},
|
|||
|
|
date: "۱۸ آذر ۱۴۰۳",
|
|||
|
|
verified: true,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "3",
|
|||
|
|
rating: 5,
|
|||
|
|
title: "عالی برای استفاده روزمره",
|
|||
|
|
content:
|
|||
|
|
"یک ماه است هر روز از آن استفاده میکنم و هنوز مثل روز اول به نظر میرسد و درست کار میکند. با این قیمت، کیفیت ساخت واقعاً قابل تحسین است. برای خواهرم هم یکی خریدم.",
|
|||
|
|
author: {
|
|||
|
|
name: "نیلوفر حسینی",
|
|||
|
|
avatar: "/block-media/shadcnblocks/block/avatar-3.webp",
|
|||
|
|
},
|
|||
|
|
date: "۱۵ آذر ۱۴۰۳",
|
|||
|
|
verified: true,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "4",
|
|||
|
|
rating: 4,
|
|||
|
|
title: "خوب، ولی کامل نیست",
|
|||
|
|
content:
|
|||
|
|
"محصول خوب است و درست کار میکند. تنها نکته کوچک این است که رنگ کمی با عکسها فرق دارد، ولی در مجموع خرید رضایتبخشی بود.",
|
|||
|
|
author: {
|
|||
|
|
name: "امیر محمدی",
|
|||
|
|
},
|
|||
|
|
date: "۱۲ آذر ۱۴۰۳",
|
|||
|
|
verified: false,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "5",
|
|||
|
|
rating: 5,
|
|||
|
|
title: "بهترین خرید امسالم",
|
|||
|
|
content:
|
|||
|
|
"واقعاً عاشقش شدم! به جزئیات با دقت زیادی توجه شده. وقتی سوال داشتم، پشتیبانی هم بسیار مفید بود. از هر نظر پنج ستاره.",
|
|||
|
|
author: {
|
|||
|
|
name: "رؤیا کاظمی",
|
|||
|
|
avatar: "/block-media/shadcnblocks/block/avatar-1.webp",
|
|||
|
|
},
|
|||
|
|
date: "۸ آذر ۱۴۰۳",
|
|||
|
|
verified: true,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "6",
|
|||
|
|
rating: 3,
|
|||
|
|
title: "قابل قبول، ولی انتظار بیشتری داشتم",
|
|||
|
|
content:
|
|||
|
|
"با توجه به قیمت، محصول معمولی است؛ چیز خاصی نیست ولی کارش را انجام میدهد. ارسال سریع بود که نکته مثبتش بود.",
|
|||
|
|
author: {
|
|||
|
|
name: "پارسا موسوی",
|
|||
|
|
},
|
|||
|
|
date: "۵ آذر ۱۴۰۳",
|
|||
|
|
verified: true,
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
interface Reviews2Props {
|
|||
|
|
reviews?: Review[];
|
|||
|
|
title?: string;
|
|||
|
|
className?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const Reviews2 = ({
|
|||
|
|
reviews = DEFAULT_REVIEWS,
|
|||
|
|
title = "نظرات مشتریان",
|
|||
|
|
className,
|
|||
|
|
}: Reviews2Props) => {
|
|||
|
|
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 ratingCounts = [5, 4, 3, 2, 1].map((star) => ({
|
|||
|
|
star,
|
|||
|
|
count: reviews.filter((r) => r.rating === star).length,
|
|||
|
|
percentage:
|
|||
|
|
(reviews.filter((r) => r.rating === star).length / totalReviews) * 100,
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<section
|
|||
|
|
dir="rtl"
|
|||
|
|
lang="fa"
|
|||
|
|
className={cn("py-16 md:py-24", className)}
|
|||
|
|
>
|
|||
|
|
<div className="container max-w-5xl">
|
|||
|
|
<h2 className="mb-8 text-xl font-semibold tracking-tight md:text-3xl">
|
|||
|
|
{title}
|
|||
|
|
</h2>
|
|||
|
|
|
|||
|
|
<div className="grid gap-8 lg:grid-cols-[280px_1fr] lg:gap-12">
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
<div className="text-center lg:text-start">
|
|||
|
|
<div className="text-2xll font-bold">{formattedAverage}</div>
|
|||
|
|
<Rating
|
|||
|
|
rate={averageRating}
|
|||
|
|
className="mt-2 justify-center lg:justify-start [&_svg]:size-5"
|
|||
|
|
/>
|
|||
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|||
|
|
بر اساس {totalReviews.toLocaleString("fa-IR")} نظر
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{ratingCounts.map(({ star, count, percentage }) => (
|
|||
|
|
<div key={star} className="flex items-center gap-3">
|
|||
|
|
<span className="w-10 shrink-0 text-sm text-muted-foreground">
|
|||
|
|
{star.toLocaleString("fa-IR")} ★
|
|||
|
|
</span>
|
|||
|
|
<Progress value={percentage} className="h-2 flex-1" />
|
|||
|
|
<span className="w-8 shrink-0 text-end text-sm text-muted-foreground">
|
|||
|
|
{count.toLocaleString("fa-IR")}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|||
|
|
{reviews.map((review) => (
|
|||
|
|
<Card key={review.id} className="gap-0 p-0 shadow-none">
|
|||
|
|
<CardContent className="p-5">
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<div className="flex items-start justify-between gap-2">
|
|||
|
|
<div className="flex items-center gap-2.5">
|
|||
|
|
<Avatar className="size-9">
|
|||
|
|
<AvatarImage
|
|||
|
|
src={review.author.avatar}
|
|||
|
|
alt={review.author.name}
|
|||
|
|
/>
|
|||
|
|
<AvatarFallback className="text-xs">
|
|||
|
|
{review.author.name
|
|||
|
|
.split(/\s+/)
|
|||
|
|
.slice(0, 2)
|
|||
|
|
.map((part) => part[0])
|
|||
|
|
.join("")}
|
|||
|
|
</AvatarFallback>
|
|||
|
|
</Avatar>
|
|||
|
|
<div>
|
|||
|
|
<div className="flex items-center gap-1.5">
|
|||
|
|
<span className="text-sm font-medium">
|
|||
|
|
{review.author.name}
|
|||
|
|
</span>
|
|||
|
|
{review.verified && (
|
|||
|
|
<BadgeCheck
|
|||
|
|
className="size-4 text-emerald-600"
|
|||
|
|
aria-label="خرید تأییدشده"
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<p className="text-xs text-muted-foreground">
|
|||
|
|
{review.date}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<Rating
|
|||
|
|
rate={review.rating}
|
|||
|
|
className="[&_svg]:size-3.5 [&>div]:size-3.5"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<h3 className="font-medium">{review.title}</h3>
|
|||
|
|
<p className="mt-1.5 line-clamp-3 text-sm leading-relaxed text-muted-foreground">
|
|||
|
|
{review.content}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export { Reviews2 };
|