Files
reui-blocks/blocks/reviews5.tsx
T

309 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 };