1162 lines
35 KiB
TypeScript
1162 lines
35 KiB
TypeScript
"use client";
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { Minus, Plus, ShoppingBag, Star, X } from "lucide-react";
|
|
import { useMemo, useState } from "react";
|
|
import type { ControllerRenderProps } from "react-hook-form";
|
|
import { useForm } from "react-hook-form";
|
|
import { Controller } from "react-hook-form";
|
|
import z from "zod";
|
|
|
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardTitle } from "@/components/ui/card";
|
|
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
|
|
import { Field, FieldLabel } from "@/components/ui/field";
|
|
import { Input } from "@/components/ui/input";
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const STOCK_STATUS = {
|
|
IN_STOCK: "IN_STOCK",
|
|
OUT_OF_STOCK: "OUT_OF_STOCK",
|
|
} as const;
|
|
|
|
type StockStatusCode = keyof typeof STOCK_STATUS;
|
|
|
|
type FormType = z.infer<typeof formSchema>;
|
|
type FieldName = keyof FormType;
|
|
|
|
type Option = {
|
|
id: string;
|
|
label: string;
|
|
value: string;
|
|
stockStatusCode: StockStatusCode;
|
|
color?: string;
|
|
};
|
|
|
|
interface Hinges {
|
|
label: string;
|
|
id: string;
|
|
name: FieldName;
|
|
options?: Option[];
|
|
min?: number;
|
|
max?: number;
|
|
}
|
|
|
|
type HingesMap = Record<FieldName, Hinges>;
|
|
|
|
interface ProductPrice {
|
|
regular: number;
|
|
sale?: number;
|
|
currency: string;
|
|
discount?: string;
|
|
}
|
|
|
|
interface ProductFormProps {
|
|
hinges: HingesMap;
|
|
onOpenChange: () => void;
|
|
}
|
|
|
|
interface QuantityProps {
|
|
field: ControllerRenderProps<FormType>;
|
|
max?: number;
|
|
min?: number;
|
|
}
|
|
|
|
type Image = {
|
|
srcset?: string;
|
|
src: string;
|
|
sizes?: string;
|
|
alt: string;
|
|
};
|
|
|
|
interface Product {
|
|
link: string;
|
|
images: [Image, Image];
|
|
name: string;
|
|
price: ProductPrice;
|
|
rate: number;
|
|
hinges?: HingesMap;
|
|
}
|
|
|
|
type ProductCardProps = Product;
|
|
|
|
interface RadioGroupProps {
|
|
options?: Array<Option>;
|
|
id: string;
|
|
field: ControllerRenderProps<FormType>;
|
|
}
|
|
|
|
type ProductList = Array<Product>;
|
|
|
|
const PRODUCTS_LIST: ProductList = [
|
|
{
|
|
name: "پیراهن میدی راهراه عمودی",
|
|
price: {
|
|
regular: 990000.0,
|
|
sale: 790000.0,
|
|
currency: "ریال",
|
|
discount: "-۲۰٪",
|
|
},
|
|
rate: 5,
|
|
link: "#",
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555009-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555009-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555009-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555009-1.jpg 640w",
|
|
alt: "پیراهن میدی راهراه عمودی",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555003-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555003-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555003-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555003-1.jpg 640w",
|
|
alt: "نمای دیگر پیراهن میدی راهراه",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
hinges: {
|
|
size: {
|
|
label: "سایز",
|
|
id: "size",
|
|
name: "size",
|
|
options: [
|
|
{
|
|
id: "xs-1",
|
|
value: "xs",
|
|
label: "xs",
|
|
stockStatusCode: "OUT_OF_STOCK",
|
|
},
|
|
{
|
|
id: "s-1",
|
|
value: "s",
|
|
label: "s",
|
|
stockStatusCode: "OUT_OF_STOCK",
|
|
},
|
|
{
|
|
id: "m-1",
|
|
value: "m",
|
|
label: "m",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "l-1",
|
|
value: "l",
|
|
label: "l",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "xl-1",
|
|
value: "xl",
|
|
label: "xl",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "xxl-1",
|
|
value: "xxl",
|
|
label: "xxl",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "3xl-1",
|
|
value: "3xl",
|
|
label: "3xl",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "4xl-1",
|
|
value: "4xl",
|
|
label: "4xl",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
],
|
|
},
|
|
color: {
|
|
label: "رنگ",
|
|
id: "color",
|
|
name: "color",
|
|
options: [
|
|
{
|
|
id: "black-1",
|
|
label: "مشکی",
|
|
value: "black",
|
|
color: "#000000",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "blue-1",
|
|
label: "آبی",
|
|
value: "blue",
|
|
color: "#607FC7",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "red-1",
|
|
label: "قرمز",
|
|
value: "red",
|
|
color: "#97012e",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
],
|
|
},
|
|
quantity: {
|
|
label: "تعداد",
|
|
id: "quantity-1",
|
|
name: "quantity",
|
|
min: 1,
|
|
max: 90,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "پیراهن میدی بندی",
|
|
price: {
|
|
regular: 990000.0,
|
|
currency: "ریال",
|
|
},
|
|
rate: 4.5,
|
|
link: "#",
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555022-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555022-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555022-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555022-1.jpg 640w",
|
|
alt: "پیراهن میدی بندی",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555033-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555033-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555033-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-15555033-1.jpg 640w",
|
|
alt: "نمای دیگر پیراهن میدی بندی",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
hinges: {
|
|
size: {
|
|
label: "سایز",
|
|
id: "size",
|
|
name: "size",
|
|
options: [
|
|
{
|
|
id: "xs-2",
|
|
value: "xs",
|
|
label: "xs",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "s-2",
|
|
value: "s",
|
|
label: "s",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "m-2",
|
|
value: "m",
|
|
label: "m",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "l-2",
|
|
value: "l",
|
|
label: "l",
|
|
stockStatusCode: "OUT_OF_STOCK",
|
|
},
|
|
],
|
|
},
|
|
color: {
|
|
label: "رنگ",
|
|
id: "color",
|
|
name: "color",
|
|
options: [
|
|
{
|
|
id: "black-2",
|
|
label: "مشکی",
|
|
value: "black",
|
|
color: "#000000",
|
|
stockStatusCode: "OUT_OF_STOCK",
|
|
},
|
|
{
|
|
id: "white-2",
|
|
label: "سفید",
|
|
value: "white",
|
|
color: "#FFFFFF",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
],
|
|
},
|
|
quantity: {
|
|
label: "تعداد",
|
|
id: "quantity-2",
|
|
name: "quantity",
|
|
min: 1,
|
|
max: 90,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "شرتدرس میدی کمربنددار",
|
|
price: {
|
|
regular: 149000.0,
|
|
currency: "ریال",
|
|
},
|
|
rate: 4.5,
|
|
link: "#",
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19745200-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19745200-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19745200-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19745200-1.jpg 640w",
|
|
alt: "شرتدرس میدی کمربنددار",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19745178-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19745178-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19745178-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19745178-1.jpg 640w",
|
|
alt: "نمای دیگر شرتدرس میدی کمربنددار",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
hinges: {
|
|
size: {
|
|
label: "سایز",
|
|
id: "size",
|
|
name: "size",
|
|
options: [
|
|
{
|
|
id: "xs-3",
|
|
value: "xs",
|
|
label: "xs",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "s-3",
|
|
value: "s",
|
|
label: "s",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "m-3",
|
|
value: "m",
|
|
label: "m",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "l-3",
|
|
value: "l",
|
|
label: "l",
|
|
stockStatusCode: "OUT_OF_STOCK",
|
|
},
|
|
],
|
|
},
|
|
color: {
|
|
label: "رنگ",
|
|
id: "color",
|
|
name: "color",
|
|
options: [
|
|
{
|
|
id: "black-3",
|
|
label: "مشکی",
|
|
value: "black",
|
|
color: "#000000",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "blue-3",
|
|
label: "آبی",
|
|
value: "blue",
|
|
color: "#607FC7",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
],
|
|
},
|
|
quantity: {
|
|
label: "تعداد",
|
|
id: "quantity-3",
|
|
name: "quantity",
|
|
min: 1,
|
|
max: 90,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "تاپ آستین بلند",
|
|
price: {
|
|
regular: 490000.0,
|
|
currency: "ریال",
|
|
},
|
|
rate: 4.5,
|
|
link: "#",
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881407-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881407-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881407-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881407-1.jpg 640w",
|
|
alt: "تاپ آستین بلند صورتی",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881409-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881409-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881409-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881409-1.jpg 640w",
|
|
alt: "نمای دیگر تاپ آستین بلند",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
hinges: {
|
|
size: {
|
|
label: "سایز",
|
|
id: "size",
|
|
name: "size",
|
|
options: [
|
|
{
|
|
id: "xs-4",
|
|
value: "xs",
|
|
label: "xs",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "s-4",
|
|
value: "s",
|
|
label: "s",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "m-4",
|
|
value: "m",
|
|
label: "m",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "l-4",
|
|
value: "l",
|
|
label: "l",
|
|
stockStatusCode: "OUT_OF_STOCK",
|
|
},
|
|
],
|
|
},
|
|
color: {
|
|
label: "رنگ",
|
|
id: "color",
|
|
name: "color",
|
|
options: [
|
|
{
|
|
id: "pink-4",
|
|
label: "صورتی",
|
|
value: "pink",
|
|
color: "#F1E1E1",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
],
|
|
},
|
|
quantity: {
|
|
label: "تعداد",
|
|
id: "quantity-4",
|
|
name: "quantity",
|
|
min: 1,
|
|
max: 90,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "تاپ آستین بلند",
|
|
price: {
|
|
regular: 490000.0,
|
|
currency: "ریال",
|
|
},
|
|
rate: 4.5,
|
|
link: "#",
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881457-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881457-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881457-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881457-1.jpg 640w",
|
|
alt: "تاپ آستین بلند کرم",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881454-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881454-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881454-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-19881454-1.jpg 640w",
|
|
alt: "نمای دیگر تاپ آستین بلند کرم",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
hinges: {
|
|
size: {
|
|
label: "سایز",
|
|
id: "size",
|
|
name: "size",
|
|
options: [
|
|
{
|
|
id: "xs-5",
|
|
value: "xs",
|
|
label: "xs",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "s-5",
|
|
value: "s",
|
|
label: "s",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "m-5",
|
|
value: "m",
|
|
label: "m",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "l-5",
|
|
value: "l",
|
|
label: "l",
|
|
stockStatusCode: "OUT_OF_STOCK",
|
|
},
|
|
],
|
|
},
|
|
color: {
|
|
label: "رنگ",
|
|
id: "color",
|
|
name: "color",
|
|
options: [
|
|
{
|
|
id: "black-5",
|
|
label: "مشکی",
|
|
value: "black",
|
|
color: "#000000",
|
|
stockStatusCode: "OUT_OF_STOCK",
|
|
},
|
|
{
|
|
id: "white-5",
|
|
label: "سفید",
|
|
value: "white",
|
|
color: "#FFFFFF",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "pink-5",
|
|
label: "صورتی",
|
|
value: "pink",
|
|
color: "#F1E1E1",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
],
|
|
},
|
|
quantity: {
|
|
label: "تعداد",
|
|
id: "quantity-5",
|
|
name: "quantity",
|
|
min: 1,
|
|
max: 90,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "شرتدرس میدی کمربنددار",
|
|
price: {
|
|
regular: 990000.0,
|
|
currency: "ریال",
|
|
},
|
|
rate: 4.5,
|
|
link: "#",
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851438-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851438-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851438-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851438-1.jpg 640w",
|
|
alt: "شرتدرس میدی کمربنددار بژ",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851442-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851442-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851442-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851442-1.jpg 640w",
|
|
alt: "نمای دیگر شرتدرس میدی بژ",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
hinges: {
|
|
size: {
|
|
label: "سایز",
|
|
id: "size",
|
|
name: "size",
|
|
options: [
|
|
{
|
|
id: "xs-6",
|
|
value: "xs",
|
|
label: "xs",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "s-6",
|
|
value: "s",
|
|
label: "s",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "m-6",
|
|
value: "m",
|
|
label: "m",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "l-6",
|
|
value: "l",
|
|
label: "l",
|
|
stockStatusCode: "OUT_OF_STOCK",
|
|
},
|
|
],
|
|
},
|
|
color: {
|
|
label: "رنگ",
|
|
id: "color",
|
|
name: "color",
|
|
options: [
|
|
{
|
|
id: "beige-6",
|
|
label: "بژ",
|
|
value: "beige",
|
|
color: "#EDE8EC",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "white-6",
|
|
label: "سفید",
|
|
value: "white",
|
|
color: "#FFFFFF",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
],
|
|
},
|
|
quantity: {
|
|
label: "تعداد",
|
|
id: "quantity-6",
|
|
name: "quantity",
|
|
min: 1,
|
|
max: 90,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "پالتو بارانی",
|
|
price: {
|
|
regular: 199000.0,
|
|
sale: 990000.0,
|
|
discount: "-۵۰٪",
|
|
currency: "ریال",
|
|
},
|
|
rate: 4.5,
|
|
link: "#",
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851622-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851622-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851622-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851622-1.jpg 640w",
|
|
alt: "پالتو بارانی",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851458-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851458-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851458-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-429124762-20851458-1.jpg 640w",
|
|
alt: "نمای دیگر پالتو بارانی",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
hinges: {
|
|
size: {
|
|
label: "سایز",
|
|
id: "size",
|
|
name: "size",
|
|
options: [
|
|
{
|
|
id: "xs-7",
|
|
value: "xs",
|
|
label: "xs",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "s-7",
|
|
value: "s",
|
|
label: "s",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "m-7",
|
|
value: "m",
|
|
label: "m",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "l-7",
|
|
value: "l",
|
|
label: "l",
|
|
stockStatusCode: "OUT_OF_STOCK",
|
|
},
|
|
],
|
|
},
|
|
color: {
|
|
label: "رنگ",
|
|
id: "color",
|
|
name: "color",
|
|
options: [
|
|
{
|
|
id: "black-7",
|
|
label: "مشکی",
|
|
value: "black",
|
|
color: "#000000",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
{
|
|
id: "beige-7",
|
|
label: "بژ",
|
|
value: "beige",
|
|
color: "#EDE8EC",
|
|
stockStatusCode: "IN_STOCK",
|
|
},
|
|
],
|
|
},
|
|
quantity: {
|
|
label: "تعداد",
|
|
id: "quantity-7",
|
|
name: "quantity",
|
|
min: 1,
|
|
max: 90,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "کیف مینی چرم مصنوعی",
|
|
price: {
|
|
regular: 990000.0,
|
|
currency: "ریال",
|
|
},
|
|
rate: 4.5,
|
|
link: "#",
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-dhanno-22432995-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-dhanno-22432995-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-dhanno-22432995-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-dhanno-22432995-1.jpg 640w",
|
|
alt: "کیف مینی چرم مصنوعی",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/clothes/pexels-dhanno-22432980-2.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/clothes/pexels-dhanno-22432980-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-dhanno-22432980-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/clothes/pexels-dhanno-22432980-1.jpg 640w",
|
|
alt: "نمای دیگر کیف مینی چرم مصنوعی",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
interface ProductList8Props {
|
|
className?: string;
|
|
}
|
|
|
|
const ProductList8 = ({ className }: ProductList8Props) => {
|
|
return (
|
|
<section className={cn("py-32", className)}>
|
|
<div className="grid grid-cols-1 gap-px sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
|
{PRODUCTS_LIST.map((item, index) => (
|
|
<ProductCard key={`product-list-8-card-${index}`} {...item} />
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const ProductCard = ({
|
|
link,
|
|
images,
|
|
name,
|
|
price,
|
|
rate,
|
|
hinges,
|
|
}: ProductCardProps) => {
|
|
const [open, setOpen] = useState(false);
|
|
const { regular, sale, currency } = price;
|
|
|
|
const onOpenChange = () => {
|
|
if (hinges) {
|
|
setOpen(!open);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className="group relative block rounded-none border-none py-5 shadow-none ring ring-border">
|
|
<a href={link} className="absolute inset-0 z-50 size-full"></a>
|
|
<CardContent>
|
|
<div className="relative overflow-hidden">
|
|
<AspectRatio
|
|
ratio={0.833342901}
|
|
className="overflow-hidden rounded-[1.25rem]"
|
|
>
|
|
{images.map((img, index) => (
|
|
<div
|
|
className="absolute inset-0 size-full transition-opacity duration-350 first:z-20 group-hover:first:opacity-0 last:z-10 first:[&>img]:scale-110 first:[&>img]:delay-50 group-hover:first:[&>img]:scale-100 last:[&>img]:scale-110 group-hover:last:[&>img]:scale-100"
|
|
key={`product-list-8-card-img-${index}`}
|
|
>
|
|
<img
|
|
src={img.src}
|
|
srcSet={img.srcset}
|
|
alt={img.alt}
|
|
sizes={img.sizes}
|
|
className="block size-full object-cover object-center transition-transform duration-350"
|
|
/>
|
|
</div>
|
|
))}
|
|
</AspectRatio>
|
|
<Badge
|
|
variant="outline"
|
|
className="absolute start-4 top-4 z-30 rounded-full bg-background font-bold opacity-0 transition-opacity duration-350 group-hover:opacity-100"
|
|
>
|
|
<Star className="fill-inherit" />
|
|
{rate.toLocaleString("fa-IR")}
|
|
</Badge>
|
|
{price.discount && (
|
|
<Badge
|
|
variant="outline"
|
|
className="absolute end-4 top-4 z-30 rounded-full bg-background font-bold text-red-600 transition-opacity duration-350"
|
|
>
|
|
{price.discount}
|
|
</Badge>
|
|
)}
|
|
<Button
|
|
size="icon"
|
|
onClick={onOpenChange}
|
|
className="absolute end-4 bottom-4 z-60 rounded-full lg:translate-y-[calc(100%+1rem)] lg:group-hover:translate-y-0"
|
|
>
|
|
<ShoppingBag />
|
|
</Button>
|
|
</div>
|
|
<div className="space-y-0.5 pt-3.5">
|
|
<CardTitle className="leading-relaxed">{name}</CardTitle>
|
|
<ProductCardPrice regular={regular} sale={sale} currency={currency} />
|
|
</div>
|
|
</CardContent>
|
|
{hinges && (
|
|
<Collapsible open={open} onOpenChange={onOpenChange}>
|
|
<CollapsibleContent className="absolute inset-0 z-70 size-full bg-background">
|
|
<ProductForm onOpenChange={onOpenChange} hinges={hinges} />
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)}
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
const formSchema = z.object({
|
|
size: z.string(),
|
|
color: z.string(),
|
|
quantity: z.number(),
|
|
});
|
|
|
|
const ProductForm = ({ onOpenChange, hinges }: ProductFormProps) => {
|
|
const form = useForm<FormType>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
color: "",
|
|
size: "",
|
|
quantity: 1,
|
|
},
|
|
});
|
|
|
|
function onSubmit(values: FormType) {
|
|
console.log(values);
|
|
}
|
|
|
|
const sizeHinges = hinges?.size;
|
|
const colorHinges = hinges?.color;
|
|
const quantityHinges = hinges?.quantity;
|
|
|
|
const currentColorId = form.watch("color");
|
|
const currentColor = useMemo(
|
|
() =>
|
|
colorHinges?.options?.find((item) => item.value === currentColorId)
|
|
?.label ?? "",
|
|
[currentColorId, colorHinges],
|
|
);
|
|
|
|
return (
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="h-full">
|
|
<div className="relative h-full">
|
|
<ScrollArea className="h-full pb-30 lg:pb-17">
|
|
<div className="flex size-full flex-col gap-6">
|
|
<div className="relative space-y-5 p-4">
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
type="button"
|
|
onClick={onOpenChange}
|
|
className="absolute end-2 top-2 rounded-full"
|
|
>
|
|
<X />
|
|
</Button>
|
|
{sizeHinges && (
|
|
<Controller
|
|
control={form.control}
|
|
name={sizeHinges.name}
|
|
render={({ field }) => (
|
|
<Field>
|
|
<FieldLabel
|
|
className="text-base leading-normal font-bold"
|
|
asChild
|
|
>
|
|
<h2>{sizeHinges.label}</h2>
|
|
</FieldLabel>
|
|
<div className="relative -me-4 overflow-hidden after:pointer-events-none after:absolute after:end-0 after:top-0 after:block after:h-full after:w-10.5 after:bg-gradient-to-l after:from-transparent after:to-background after:content-['']">
|
|
<SizeRadioGroup
|
|
field={field}
|
|
options={sizeHinges.options}
|
|
id={sizeHinges.id}
|
|
/>
|
|
</div>
|
|
</Field>
|
|
)}
|
|
/>
|
|
)}
|
|
{colorHinges && (
|
|
<Controller
|
|
control={form.control}
|
|
name={colorHinges.name}
|
|
render={({ field }) => (
|
|
<Field>
|
|
<FieldLabel
|
|
className="text-base leading-normal font-bold"
|
|
asChild
|
|
>
|
|
<h2>{colorHinges.label}</h2>
|
|
</FieldLabel>
|
|
<ColorRadioGroup
|
|
field={field}
|
|
options={colorHinges.options}
|
|
id={colorHinges.id}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{currentColor
|
|
? currentColor
|
|
: `${colorHinges.options?.length.toLocaleString("fa-IR")} رنگ موجود`}
|
|
</p>
|
|
</Field>
|
|
)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
<div className="absolute inset-x-0 bottom-0">
|
|
<div className="flex flex-col items-center gap-4 bg-background p-4 lg:flex-row">
|
|
{quantityHinges && (
|
|
<Controller
|
|
control={form.control}
|
|
name={quantityHinges.name}
|
|
render={({ field }) => (
|
|
<Field className="self-stretch">
|
|
<Quantity
|
|
field={field}
|
|
min={quantityHinges?.min}
|
|
max={quantityHinges?.max}
|
|
/>
|
|
</Field>
|
|
)}
|
|
/>
|
|
)}
|
|
<div className="flex-1 self-stretch">
|
|
<Button
|
|
disabled={!form.getValues("color") && !form.getValues("size")}
|
|
className="w-full rounded-full"
|
|
>
|
|
افزودن به سبد
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
const SizeRadioGroup = ({ options, id, field }: RadioGroupProps) => {
|
|
if (!options) return;
|
|
|
|
return (
|
|
<RadioGroup
|
|
{...field}
|
|
value={`${field.value}`}
|
|
id={id}
|
|
onValueChange={(value) => {
|
|
if (value != field.value && value) {
|
|
field.onChange(value);
|
|
}
|
|
}}
|
|
className="flex w-full items-center gap-2 overflow-x-auto p-px [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
>
|
|
{options.map((item, index) => (
|
|
<Field
|
|
className="shrink-0 last:pe-14.5"
|
|
key={`product-list-8-size-${index}`}
|
|
>
|
|
<FieldLabel
|
|
htmlFor={item.id}
|
|
className="h-9 cursor-pointer overflow-hidden rounded-full border px-4.5 py-3 text-center leading-none font-normal uppercase ring-primary duration-400 not-has-disabled:hover:ring-1 has-checked:ring has-checked:hover:ring has-disabled:opacity-25"
|
|
>
|
|
<RadioGroupItem
|
|
id={item.id}
|
|
className="sr-only"
|
|
value={item.value}
|
|
aria-label={`انتخاب ${item.label}`}
|
|
disabled={item.stockStatusCode === STOCK_STATUS.OUT_OF_STOCK}
|
|
/>
|
|
{item.label}
|
|
</FieldLabel>
|
|
</Field>
|
|
))}
|
|
</RadioGroup>
|
|
);
|
|
};
|
|
|
|
const ColorRadioGroup = ({ options, id, field }: RadioGroupProps) => {
|
|
if (!options) return;
|
|
|
|
return (
|
|
<RadioGroup
|
|
{...field}
|
|
value={`${field.value}`}
|
|
id={id}
|
|
onValueChange={(value) => {
|
|
if (value != field.value && value) {
|
|
field.onChange(value);
|
|
}
|
|
}}
|
|
className="flex w-full flex-wrap items-center gap-2 overflow-x-auto p-px [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
>
|
|
{options.map((item, index) => (
|
|
<Field key={`product-list-8-color-${index}`}>
|
|
<FieldLabel
|
|
htmlFor={item.id}
|
|
className="relative flex size-9 cursor-pointer overflow-hidden rounded-full border ring-primary duration-400 not-has-disabled:hover:ring has-checked:ring has-disabled:opacity-25"
|
|
>
|
|
<RadioGroupItem
|
|
id={item.id}
|
|
className="sr-only"
|
|
value={item.value}
|
|
aria-label={`انتخاب ${item.label}`}
|
|
disabled={item.stockStatusCode === STOCK_STATUS.OUT_OF_STOCK}
|
|
/>
|
|
<div
|
|
style={{
|
|
backgroundColor: item.color,
|
|
}}
|
|
className="m-auto size-7.5 rounded-full"
|
|
></div>
|
|
{item.stockStatusCode === STOCK_STATUS.OUT_OF_STOCK && (
|
|
<span className="absolute top-0 bottom-0 start-1/2 z-10 block h-full w-0 rotate-45 border-s"></span>
|
|
)}
|
|
</FieldLabel>
|
|
</Field>
|
|
))}
|
|
</RadioGroup>
|
|
);
|
|
};
|
|
|
|
const Quantity = ({ field, max, min }: QuantityProps) => {
|
|
return (
|
|
<div
|
|
className="flex h-9 w-full min-w-34 items-center overflow-hidden rounded-full border shadow-xs"
|
|
aria-label="تعداد"
|
|
>
|
|
<Button
|
|
onClick={() =>
|
|
field.onChange(Math.max(min || 1, Number(field.value || 1) - 1))
|
|
}
|
|
variant="ghost"
|
|
type="button"
|
|
size="icon"
|
|
className="shrink-0 rounded-none"
|
|
>
|
|
<Minus />
|
|
</Button>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
onChange={(e) => {
|
|
const raw = e.target.value;
|
|
const parsed = parseInt(raw, 10);
|
|
|
|
// Accept empty string for typing, but validate if not a number
|
|
if (raw === "") {
|
|
field.onChange("");
|
|
} else if (!isNaN(parsed)) {
|
|
field.onChange(parsed);
|
|
}
|
|
}}
|
|
type="number"
|
|
min={Math.max(min ?? 1, 1)}
|
|
max={max ?? 99}
|
|
className="h-full w-full min-w-10 flex-1 [appearance:textfield] rounded-none border-0 px-1 text-center shadow-none focus-visible:ring-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
/>
|
|
<Button
|
|
onClick={() =>
|
|
field.onChange(Math.min(max || 99, Number(field.value || 1) + 1))
|
|
}
|
|
variant="ghost"
|
|
type="button"
|
|
size="icon"
|
|
className="shrink-0 rounded-none"
|
|
>
|
|
<Plus />
|
|
</Button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const formatCurrency = (
|
|
value: number,
|
|
currency: string = "ریال",
|
|
locale: string = "fa-IR",
|
|
) => {
|
|
return `${value.toLocaleString(locale)} ${currency}`;
|
|
};
|
|
|
|
const ProductCardPrice = ({
|
|
regular,
|
|
sale,
|
|
currency,
|
|
}: {
|
|
regular: number;
|
|
sale?: number;
|
|
currency: string;
|
|
}) => {
|
|
const onSale = sale != null;
|
|
|
|
return (
|
|
<div className="flex flex-wrap items-center gap-x-2">
|
|
{onSale ? (
|
|
<span className="leading-tight text-foreground">
|
|
{formatCurrency(sale, currency)}
|
|
</span>
|
|
) : null}
|
|
<span
|
|
className={cn(
|
|
"leading-tight",
|
|
onSale
|
|
? "text-muted-foreground line-through"
|
|
: "text-foreground",
|
|
)}
|
|
>
|
|
{formatCurrency(regular, currency)}
|
|
</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export { ProductList8 };
|