Files
reui-blocks/blocks/reviews5.tsx
T

309 lines
10 KiB
TypeScript
Raw Normal View History

"use client";
import { BadgeCheck, ChevronDown } from "lucide-react";
import { useState } from "react";
import { Rating } from "@/components/shadcnblocks/rating";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Progress } from "@/components/ui/progress";
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;
/** برای مرتب‌سازی جدیدترین؛ تاریخ نمایشی ممکن است شمسی باشد */
postedAt?: number;
verified?: boolean;
helpful?: number;
}
const DEFAULT_REVIEWS: Review[] = [
{
id: "1",
rating: 5,
title: "فراتر از انتظارم بود",
content:
"اول کمی تردید داشتم، ولی این محصول واقعاً جواب داد. کیفیت ساخت عالی است و زودتر از موعد به دستم رسید.",
author: {
name: "سارا احمدی",
avatar: "/block-media/shadcnblocks/block/avatar-1.webp",
},
date: "۲۰ آذر ۱۴۰۳",
postedAt: 6,
verified: true,
helpful: 42,
},
{
id: "2",
rating: 5,
title: "بهترین خرید امسالم",
content:
"واقعاً عاشقش شدم! به جزئیات با دقت زیادی توجه شده. وقتی سوال داشتم، پشتیبانی هم بسیار مفید بود.",
author: {
name: "مازیار رضایی",
avatar: "/block-media/shadcnblocks/block/avatar-2.webp",
},
date: "۱۸ آذر ۱۴۰۳",
postedAt: 5,
verified: true,
helpful: 38,
},
{
id: "3",
rating: 4,
title: "ارزش خرید بالا",
content:
"به‌طور کلی محصول خوبی است و دقیقاً همان کاری را می‌کند که باید. پیشنهاد می‌کنم.",
author: {
name: "نیلوفر حسینی",
avatar: "/block-media/shadcnblocks/block/avatar-3.webp",
},
date: "۱۵ آذر ۱۴۰۳",
postedAt: 4,
verified: true,
helpful: 24,
},
{
id: "4",
rating: 5,
title: "عالی برای استفاده روزمره",
content:
"یک ماه است هر روز از آن استفاده می‌کنم و هنوز مثل روز اول به نظر می‌رسد و درست کار می‌کند. کیفیت ساخت قابل تحسین است.",
author: { name: "امیر محمدی" },
date: "۱۲ آذر ۱۴۰۳",
postedAt: 3,
verified: false,
helpful: 19,
},
{
id: "5",
rating: 4,
title: "خوب، ولی جای بهبود دارد",
content:
"محصول خوبی است. رنگ کمی با عکس‌ها فرق داشت، ولی در مجموع از خریدم راضی‌ام.",
author: {
name: "رؤیا کاظمی",
avatar: "/block-media/shadcnblocks/block/avatar-1.webp",
},
date: "۸ آذر ۱۴۰۳",
postedAt: 2,
verified: true,
helpful: 12,
},
{
id: "6",
rating: 3,
title: "قابل قبول، ولی انتظار بیشتری داشتم",
content:
"با توجه به قیمت، محصول معمولی است؛ چیز خاصی نیست ولی کارش را انجام می‌دهد. ارسال سریع بود.",
author: { name: "پارسا موسوی" },
date: "۵ آذر ۱۴۰۳",
postedAt: 1,
verified: true,
helpful: 8,
},
];
type SortOption = "helpful" | "newest" | "highest" | "lowest";
interface Reviews5Props {
reviews?: Review[];
title?: string;
className?: string;
}
const sortLabels: Record<SortOption, string> = {
helpful: "مفیدترین",
newest: "جدیدترین",
highest: "بیشترین امتیاز",
lowest: "کمترین امتیاز",
};
const Reviews5 = ({
reviews = DEFAULT_REVIEWS,
title = "نظرات مشتریان",
className,
}: Reviews5Props) => {
const [sortBy, setSortBy] = useState<SortOption>("helpful");
const totalReviews = reviews.length;
const averageRating =
reviews.reduce((sum, r) => sum + r.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,
}));
const sortedReviews = [...reviews].sort((a, b) => {
switch (sortBy) {
case "helpful":
return (b.helpful || 0) - (a.helpful || 0);
case "newest":
return (b.postedAt ?? 0) - (a.postedAt ?? 0);
case "highest":
return b.rating - a.rating;
case "lowest":
return a.rating - b.rating;
default:
return 0;
}
});
const formatHelpfulCount = (count: number) =>
`${count.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-6 flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
<div>
<h2 className="text-xl font-semibold tracking-tight">{title}</h2>
<div className="mt-2 flex items-center gap-3">
<div className="flex items-baseline gap-1">
<span className="text-3xl font-bold">{formattedAverage}</span>
<span className="text-sm text-muted-foreground">از ۵</span>
</div>
<Rating rate={averageRating} className="[&_svg]:size-4" />
<span className="text-sm text-muted-foreground">
({totalReviews.toLocaleString("fa-IR")})
</span>
</div>
</div>
<div className="flex gap-2 sm:w-48">
<div className="flex flex-col gap-1.5 text-xs text-muted-foreground">
{[5, 4, 3, 2, 1].map((n) => (
<span key={n} className="h-2 leading-none">
{n.toLocaleString("fa-IR")}
</span>
))}
</div>
<div className="flex flex-1 flex-col gap-1.5">
{ratingCounts.map(({ star, percentage }) => (
<Progress key={star} value={percentage} className="h-2" />
))}
</div>
</div>
</div>
<Separator className="mb-4" />
<div className="mb-4 flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{totalReviews.toLocaleString("fa-IR")} نظر
</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="gap-1">
مرتب‌سازی: {sortLabels[sortBy]}
<ChevronDown className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{(Object.keys(sortLabels) as SortOption[]).map((option) => (
<DropdownMenuItem
key={option}
onClick={() => setSortBy(option)}
className={cn(sortBy === option && "font-medium")}
>
{sortLabels[option]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-0">
{sortedReviews.map((review, index) => (
<div key={review.id}>
{index > 0 && <Separator className="my-4" />}
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Avatar className="size-7">
<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>
<span className="text-sm font-medium">
{review.author.name}
</span>
{review.verified && (
<BadgeCheck
className="size-4 text-emerald-600"
aria-label="خرید تأییدشده"
/>
)}
</div>
<span className="text-xs text-muted-foreground">
{review.date}
</span>
</div>
<div className="flex items-center gap-2">
<Rating
rate={review.rating}
className="[&_svg]:size-3.5 [&>div]:size-3.5"
/>
<span className="font-medium">{review.title}</span>
</div>
<p className="text-sm text-muted-foreground">
{review.content}
</p>
{review.helpful !== undefined && review.helpful > 0 && (
<p className="text-xs text-muted-foreground">
{formatHelpfulCount(review.helpful)}
</p>
)}
</div>
</div>
))}
</div>
<div className="mt-6 text-center">
<Button variant="outline">نمایش نظرات بیشتر</Button>
</div>
</div>
</section>
);
};
export { Reviews5 };