Files
reui-blocks/blocks/reviews18.tsx
T

446 lines
14 KiB
TypeScript

"use client";
import "photoswipe/style.css";
import PhotoSwipeLightbox from "photoswipe/lightbox";
import { useCallback, useEffect, useId, useRef, useState } from "react";
import { Rating } from "@/components/shadcnblocks/rating";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { badgeVariants } from "@/components/ui/badge";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
type ImageType = {
src: string;
width: number;
height: number;
alt?: string;
};
type ReviewType = {
rate: number;
author?: string;
verifiedBuyer?: boolean;
country?: {
name: string;
code: string;
};
date: string;
title: string;
comment: string;
images?: ImageType[];
};
type ReplyType = {
title: string;
comment: string;
date: string;
};
type ReviewWithOptionalReply = ReviewType & {
reply?: ReplyType;
};
interface Reviews18Props {
reviews?: ReviewWithOptionalReply[];
pageAmmount?: string;
className?: string;
}
type ReviewProps = ReviewWithOptionalReply;
const REVIEWS: ReviewWithOptionalReply[] = [
{
rate: 5,
author: "جواد حسینی",
verifiedBuyer: true,
country: {
name: "کانادا",
code: "CA",
},
date: "۲ ماه پیش",
title: "ساعت زیبا با کیفیت عالی",
comment:
"واقعاً این ساعت را دوست دارم! حس لوکس دارد، روی مچ راحت است و حضوری حتی بهتر از عکس به نظر می‌رسد. جزئیاتش چشمگیر است و هم با استایل روزمره و هم رسمی جور درمی‌آید. سریع رسید و بسته‌بندی محکم بود. از وقتی رسیده تقریباً هر روز بسته‌ام قطعاً پیشنهاد می‌کنم.",
images: [
{
src: "/block-media/shadcnblocks/block/ecommerce/accessories/Elegant-Watch-on-Womans-Wrist-2.png",
width: 1200,
height: 1200,
alt: "ساعت روی مچ",
},
],
},
{
rate: 5,
author: "ناشناس",
verifiedBuyer: true,
country: {
name: "ایالات متحده",
code: "US",
},
date: "۲ ماه پیش",
title: "جواهرات خیره‌کننده",
comment:
"کیفیت ساخت فوق‌العاده است. جواهرات ظریف و زیر نور زیبا می‌درخشد. دقیقاً همان چیزی بود که می‌خواستم سبک، راحت و بسیار شیک. حس برند لوکس دارد و در جعبه زیبایی رسید که تجربه خرید را بهتر کرد. تا حالا چندین تعریف شنیده‌ام.",
images: [
{
src: "/block-media/shadcnblocks/block/ecommerce/accessories/Elegant-Jewelry-Portrait-1.png",
width: 349,
height: 640,
alt: "پرتره با جواهرات",
},
],
},
{
rate: 5,
author: "ناشناس",
verifiedBuyer: true,
country: {
name: "ایالات متحده",
code: "US",
},
date: "۲ ماه پیش",
title: "اندازه دقیق و طراحی شیک",
comment:
"کیفیت پارچه عالی است نرم، فیت مناسب و رنگ‌هایی که حضوری هم مثل عکس‌ها جذاب‌اند. هر دو قطعه بادوام و راحت‌اند و دوختشان دقیق است. اندازه‌ها هم دقیقاً مطابق راهنما بود. حتماً دوباره از این فروشگاه سفارش می‌دهم چون ارزش خرید واقعاً بالاست.",
images: [
{
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881454-1.jpg",
width: 640,
height: 853,
alt: "تصویر نظر مشتری لباس ۱",
},
{
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881457-1.jpg",
width: 640,
height: 853,
alt: "تصویر نظر مشتری لباس ۲",
},
],
},
{
rate: 5,
author: "ناشناس",
verifiedBuyer: true,
country: {
name: "ایالات متحده",
code: "US",
},
date: "۲ ماه پیش",
title: "کیفیت شگفت‌انگیز",
comment:
"از کیفیت کلی و بسته‌بندی خیلی راضی‌ام. همه‌چیز لوکس و بادوام است، از جنس تا جزئیات پرداخت. ارسال سریع بود و کالا سالم رسید. این برند از انتظارم بالاتر رفت و همین الان دنبال خرید بعدی‌ام.",
},
{
rate: 5,
author: "ناشناس",
verifiedBuyer: true,
country: {
name: "استرالیا",
code: "AU",
},
date: "۲ ماه پیش",
title: "کاملاً پیشنهاد می‌کنم",
comment:
"در همه جنبه‌ها از انتظارم فراتر رفت. جنس‌ها لوکس‌اند، ساخت عالی است و رنگ‌ها حضوری فوق‌العاده‌اند. وقتی درباره سایز سوال داشتم، پشتیبانی هم بسیار مفید بود. ارسال سریع، بسته‌بندی زیبا و تجربه خرید عالی از خریدم کاملاً راضی‌ام.",
},
];
const sortOptions = [
{ value: "featured", label: "برگزیده" },
{ value: "highest_rating", label: "بیشترین امتیاز" },
{ value: "lowest_rating", label: "کمترین امتیاز" },
] as const;
const Reviews18 = ({
reviews = REVIEWS,
pageAmmount = "۱ تا ۲۰ از ۲۲۵ نظر",
className,
}: Reviews18Props) => {
return (
<section
dir="rtl"
lang="fa"
className={cn("py-32", className)}
>
<div className="container">
<div className="flex items-center justify-between border-b py-4">
<p className="text-sm max-sm:sr-only">{pageAmmount}</p>
<Select defaultValue="featured">
<SelectTrigger className="w-45" aria-label="مرتب‌سازی نظرات">
<SelectValue placeholder="برگزیده" />
</SelectTrigger>
<SelectContent dir="rtl">
{sortOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
{reviews.map((item, index) => (
<Review key={`${item.author}-${item.title}-${index}`} {...item} />
))}
</div>
<div className="flex items-center justify-center">
<Button size="lg" className="rounded-full">
نمایش بیشتر
</Button>
</div>
</div>
</section>
);
};
const Review = ({
title,
comment,
images,
rate,
author,
verifiedBuyer,
country,
date,
}: ReviewProps) => {
return (
<div className="grid grid-cols-1 gap-x-10 gap-y-6 py-10 lg:grid-cols-3">
<div className="col-span-1">
<div className="space-y-2">
<Rating rate={rate} />
<div className="flex items-baseline gap-2 lg:flex-col">
<p className="text-base leading-normal font-medium text-muted-foreground lg:text-xl">
{author}
</p>
{verifiedBuyer && (
<p className="text-sm leading-normal font-medium text-green-600">
خریدار تأییدشده
</p>
)}
</div>
</div>
<div className="mt-3 lg:mt-4">
{country && (
<div className="inline-flex gap-1">
<img
alt={`پرچم ${country.name}`}
className="inline-block w-5"
src={`https://purecatamphetamine.github.io/country-flag-icons/3x2/${country.code}.svg`}
/>
<span className="text-sm text-muted-foreground">
<span>{country.name}</span>، <span>{date}</span>
</span>
</div>
)}
</div>
</div>
<div className="space-y-2 lg:col-span-2 lg:space-y-5">
<h2 className="text-lg leading-normal font-bold text-primary lg:text-xl">
{title}
</h2>
<p className="leading-relaxed text-muted-foreground">{comment}</p>
<div className="max-lg:mt-5">
<ReviewCarousel images={images} />
</div>
</div>
</div>
);
};
const ReviewCarousel = ({
images,
galleryID,
}: {
images?: ImageType[];
galleryID?: string;
}) => {
const [api, setApi] = useState<CarouselApi>();
const [current, setCurrent] = useState(0);
const lightboxRef = useRef<PhotoSwipeLightbox | null>(null);
const generatedGalleryID = useId();
const PhotoSwipeID = galleryID?.trim() ?? generatedGalleryID;
useEffect(() => {
const lightbox = new PhotoSwipeLightbox({
gallery: "#" + PhotoSwipeID,
children: "a",
bgOpacity: 1,
wheelToZoom: true,
arrowPrev: false,
arrowNext: false,
close: false,
zoom: false,
counter: false,
mainClass: "[&>div:first-child]:!bg-background",
pswpModule: () => import("photoswipe"),
});
lightbox.init();
lightboxRef.current = lightbox;
const buttonClassName = cn(
buttonVariants({
variant: "outline",
size: "icon",
className: "rounded-full",
}),
);
const indicatorClassName = cn(badgeVariants({ variant: "secondary" }));
lightbox.on("uiRegister", () => {
if (lightbox?.pswp?.ui) {
lightbox.pswp.ui.registerElement({
name: "custom-close-btn",
order: 10,
isButton: false,
appendTo: "root",
className: "absolute top-5 end-5",
html: `
<button type="button" id="pswp-close" class="${buttonClassName}" aria-label="بستن گالری">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
</button>
`,
onInit: (el, pswp) => {
el.querySelector("#pswp-close")?.addEventListener("click", () =>
pswp.close(),
);
},
});
}
if (lightbox?.pswp?.ui) {
lightbox.pswp.ui.registerElement({
name: "custom-indicator-btn",
order: 10,
isButton: false,
appendTo: "root",
className: "absolute top-5 start-5",
html: `
<div id="pswp-indicator" class="${indicatorClassName} h-8.5 px-4 !rounded-full">
</div>
`,
onInit: (el, pswp) => {
const indicatorElem = el.querySelector("#pswp-indicator");
let prevIndex = -1;
pswp.on("change", () => {
prevIndex = pswp.currIndex;
if (indicatorElem) {
const current = (prevIndex + 1).toLocaleString("fa-IR");
const total = pswp.getNumItems().toLocaleString("fa-IR");
indicatorElem.innerHTML = `${current} / ${total}`;
}
});
},
});
}
if (lightbox?.pswp?.ui) {
lightbox.pswp.ui.registerElement({
name: "custom-next-prev-btns",
order: 10,
isButton: false,
appendTo: "root",
className: "absolute top-1/2 inset-x-0 -translate-y-1/2",
html:
images && images.length < 2
? ""
: `
<div class="flex items-center justify-between px-4">
<button type="button" id="pswp-prev" class="${buttonClassName}" aria-label="تصویر بعدی">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
</button>
<button type="button" id="pswp-next" class="${buttonClassName}" aria-label="تصویر قبلی">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
</button>
</div>
`,
onInit: (el, pswp) => {
el.querySelector("#pswp-prev")?.addEventListener("click", () =>
pswp.next(),
);
el.querySelector("#pswp-next")?.addEventListener("click", () =>
pswp.prev(),
);
},
});
}
});
return () => lightbox.destroy();
}, [PhotoSwipeID, images]);
const onSelect = useCallback((carouselApi: CarouselApi) => {
if (!carouselApi) return;
setCurrent(carouselApi.selectedScrollSnap() + 1);
}, []);
useEffect(() => {
if (!api) return;
api.on("reInit", onSelect);
api.on("select", onSelect);
onSelect(api);
return () => {
api.off("select", onSelect);
};
}, [api, onSelect]);
useEffect(() => {
if (lightboxRef.current && api) {
lightboxRef.current.on("change", () => {
api.scrollTo(lightboxRef.current?.pswp?.currIndex || 0);
});
}
}, [api, current]);
if (!images?.length) return null;
return (
<div id={PhotoSwipeID}>
<Carousel setApi={setApi} opts={{ direction: "rtl", align: "start" }}>
<CarouselContent>
{images.map((img, index) => (
<CarouselItem
className="sm:basis-1/3 lg:basis-1/5"
key={`${img.src}-${index}`}
>
<AspectRatio ratio={1}>
<a
href={img.src}
data-pswp-width={img.width}
data-pswp-height={img.height}
target="_blank"
rel="noreferrer"
data-cropped="true"
>
<img
src={img.src}
alt={img.alt ?? `تصویر ${(index + 1).toLocaleString("fa-IR")}`}
className="block size-full object-cover object-center"
/>
</a>
</AspectRatio>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
);
};
export { Reviews18 };