Files
reui-blocks/blocks/product-detail9.tsx
T

454 lines
13 KiB
TypeScript

"use client";
import "photoswipe/style.css";
import { zodResolver } from "@hookform/resolvers/zod";
import PhotoSwipeLightbox from "photoswipe/lightbox";
import { useEffect, useRef, useState } from "react";
import type { ControllerRenderProps } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Button } from "@/components/ui/button";
import type { CarouselApi } from "@/components/ui/carousel";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import { Field } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { cn } from "@/lib/utils";
type StockStatusCode = "IN_STOCK" | "OUT_OF_STOCK";
interface StockInfo {
stockStatusCode: StockStatusCode;
stockQuantity?: number;
}
interface PriceProps {
regular: number;
sale?: number;
currency: string;
stockInfo?: StockInfo;
}
type option = {
id: string;
label: string;
value: string;
};
interface Hinges {
label: string;
id?: string;
name: FieldName;
options?: option[];
}
interface ProductFormProps {
hinges: Record<FieldName, Hinges>;
selected: {
size: string;
};
}
type FormType = z.infer<typeof formSchema>;
type FieldName = keyof FormType;
interface RadioGroupProps {
options?: Array<option>;
label: string;
field: ControllerRenderProps<FormType>;
}
interface ProductImagesProps {
images: Array<{
srcset: string;
src: string;
alt: string;
width: number;
height: number;
sizes: string;
}>;
galleryID: string;
}
interface ProductInfoProps {
info: Array<{
label: string;
value: string;
}>;
}
const PRODUCT_DETAILS = {
name: "پالس‌ویر ۷۶۰ ال‌ایکس طوفان خاک قاهره",
size: "8",
price: {
regular: 580000.0,
sale: 530000.0,
currency: "ریال",
},
info: [
{
label: "مدل",
value: "Pulsewear 760",
},
{
label: "استایل",
value: "CZ5594-100",
},
{
label: "سال",
value: "۲۲۵",
},
],
description:
"هنرمند مشهور بریتانیایی سلاح لوری با تراکنیت همکاری می‌کند تا سیلوئت ۷۰۰R را خلق کند. مدل تراکنیت ۷۰۰R سلاح اکلیپس مه، رویه بافتنی خاکستری طوفانی دارد که با روکش‌های سوئد آبی مه‌آلود و جزئیات ابسیدین تیره ترکیب شده است. برند تراکنیت روی پنل کناری، پاشنه و زبانه کفی دیده می‌شود. در پایان، میانسول مه‌رنگ با زیره ذغالی خال‌خالی برای کنتراست و بافت تکمیل شده است.",
hinges: {
size: {
label: "سایز",
id: "size",
name: "size" as const,
options: [
{
id: "8",
value: "8",
label: "8",
},
{
id: "8.5",
value: "8.5",
label: "8.5",
},
{
id: "9",
value: "9",
label: "9",
},
{
id: "9.5",
value: "9.5",
label: "9.5",
},
{
id: "10",
value: "10",
label: "10",
},
{
id: "10.5",
value: "10.5",
label: "10.5",
},
{
id: "11",
value: "11",
label: "11",
},
{
id: "11.5",
value: "11.5",
label: "11.5",
},
{
id: "12",
value: "12",
label: "12",
},
{
id: "13",
value: "13",
label: "13",
},
],
},
},
images: [
{
srcset:
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529146-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529146-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529146-1.jpg 640w",
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529146-3.jpg",
alt: "پالس‌ویر ۷۶۰ ال‌ایکس طوفان خاک قاهره",
width: 1920,
height: 2400,
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
},
{
srcset:
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529147-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529147-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529147-1.jpg 640w",
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529147-3.jpg",
alt: "پالس‌ویر ۷۶۰ ال‌ایکس طوفان خاک قاهره",
width: 1920,
height: 2400,
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
},
{
srcset:
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529148-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529148-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529148-1.jpg 640w",
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-melvin-buezo-1253763-2529148-3.jpg",
alt: "پالس‌ویر ۷۶۰ ال‌ایکس طوفان خاک قاهره",
width: 1920,
height: 2400,
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
},
],
};
interface ProductDetail9Props {
className?: string;
}
const ProductDetail9 = ({ className }: ProductDetail9Props) => {
return (
<section className={cn("font-ubuntu py-32", className)}>
<div className="relative grid w-full grid-cols-1 border-y border-primary lg:grid-cols-2">
<div>
<ProductImages
images={PRODUCT_DETAILS.images}
galleryID="gallery-9"
/>
</div>
<div className="sticky top-4.5 self-start py-4.5">
<div className="flex flex-col gap-9 overflow-hidden px-4.5">
<div>
<h1 className="text-3xl leading-none font-normal lg:text-2xll">
{PRODUCT_DETAILS.name}
</h1>
<div className="mt-4.5">
<Price
regular={PRODUCT_DETAILS.price.regular}
sale={PRODUCT_DETAILS.price.sale}
currency={PRODUCT_DETAILS.price.currency}
/>
</div>
</div>
<ProductForm
hinges={PRODUCT_DETAILS.hinges}
selected={{
size: PRODUCT_DETAILS.size,
}}
/>
<div className="-mx-4.5">
<ProductInfo info={PRODUCT_DETAILS.info} />
</div>
<p className="text-lg">{PRODUCT_DETAILS.description}</p>
</div>
</div>
</div>
</section>
);
};
const ProductInfo = ({ info }: ProductInfoProps) => {
if (!info) return;
return (
<ul className="flex flex-col border-t border-primary text-lg leading-tight">
{info.map((item, index) => (
<li
className="flex border-primary"
key={`product-detail-9-info-${index}`}
>
<div className="shrink-0 grow-0 basis-36 border-e border-b border-inherit px-4.5 py-2.5">
{item.label}
</div>
<div className="flex-1 border-b border-inherit px-4.5 py-2.5">
{item.value}
</div>
</li>
))}
</ul>
);
};
const ProductImages = ({ images, galleryID }: ProductImagesProps) => {
const [api, setApi] = useState<CarouselApi>();
const lightboxRef = useRef<PhotoSwipeLightbox | null>(null);
useEffect(() => {
const lightbox = new PhotoSwipeLightbox({
gallery: "#" + galleryID,
children: "a",
bgOpacity: 1,
wheelToZoom: true,
pswpModule: () => import("photoswipe"),
});
lightbox.init();
lightboxRef.current = lightbox;
return () => lightbox.destroy();
}, [galleryID]);
useEffect(() => {
if (lightboxRef.current && api) {
lightboxRef.current.on("change", () => {
api?.scrollTo(lightboxRef.current?.pswp?.currIndex || 0);
});
}
}, [api]);
if (!images) return;
return (
<div className="pswp-gallery border-e border-primary" id={galleryID}>
<Carousel
opts={{
breakpoints: {
"(min-width: 1024px)": { active: false },
},
}}
setApi={setApi}
className="w-full"
>
<CarouselContent className="ms-0 lg:flex-col">
{images.map((item, index) => {
return (
<CarouselItem
key={`product-detail-9-image-${index}`}
className="border-b border-primary ps-0 last:border-b-0"
>
<AspectRatio ratio={1}>
<a
href={item.src}
data-pswp-width={item.width}
data-pswp-height={item.height}
data-pswp-srcset={item.srcset}
target="_blank"
rel="noreferrer"
data-cropped="true"
>
<img
srcSet={item.srcset}
alt={item.alt}
sizes={item.sizes}
className="block size-full object-cover object-center"
/>
</a>
</AspectRatio>
</CarouselItem>
);
})}
</CarouselContent>
</Carousel>
</div>
);
};
const Price = ({ regular, sale, currency }: PriceProps) => {
if (!regular || !currency) return;
const formatCurrency = (
value: number,
currency: string = "ریال",
locale: string = "fa-IR",
) => {
return `${value.toLocaleString(locale)} ${currency}`;
};
return (
<div className="flex flex-wrap items-center gap-x-2.5 gap-y-1">
{sale ? (
<div
data-sale={!!sale}
aria-label="قیمت با تخفیف"
className="text-xl leading-[0.9] data-[sale=true]:text-foreground"
>
{formatCurrency(sale, currency)}
</div>
) : null}
<div
data-sale={!!sale}
aria-label="قیمت اصلی"
className="text-xl leading-[0.9] data-[sale=true]:font-normal data-[sale=true]:text-muted-foreground data-[sale=true]:line-through"
>
{formatCurrency(regular, currency)}
</div>
</div>
);
};
const formSchema = z.object({
size: z.string(),
});
const ProductForm = ({ hinges, selected }: ProductFormProps) => {
const form = useForm<FormType>({
resolver: zodResolver(formSchema),
defaultValues: {
size: selected.size,
},
});
function onSubmit(values: FormType) {
console.log(values);
}
const sizeHinges = hinges?.size;
return (
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-9"
>
<Controller
control={form.control}
name={sizeHinges.name}
render={({ field }) => (
<Field>
<h2 className="w-16 text-base leading-snug">{sizeHinges.label}</h2>
<SizeRadioGroup
field={field}
options={sizeHinges.options}
label={sizeHinges.label}
/>
</Field>
)}
/>
<div className="flex max-w-3xl flex-col gap-2.5">
<Button size="lg" variant="outline">
افزودن به سبد
</Button>
<Button size="lg">خرید فوری</Button>
</div>
</form>
);
};
const SizeRadioGroup = ({ options, field }: RadioGroupProps) => {
if (!options) return;
return (
<RadioGroup
{...field}
value={`${field.value}`}
onValueChange={(value) => {
if (value != field.value && value) {
field.onChange(value);
}
}}
className="flex w-full flex-wrap justify-start gap-2"
>
{options &&
options.map((item, index) => (
<div key={`product-detail-9-size-${index}`}>
<label
htmlFor={item.id}
className="flex h-10 min-w-10 cursor-pointer items-center justify-center rounded-md border px-2.5 text-center text-sm leading-tight font-normal shadow-xs has-checked:border-primary has-checked:bg-background"
>
<RadioGroupItem
id={item.id}
className="sr-only"
value={item.value}
aria-label={`انتخاب ${item.label}`}
/>
<div>{item.label}</div>
</label>
</div>
))}
</RadioGroup>
);
};
export { ProductDetail9 };