446 lines
14 KiB
TypeScript
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 };
|