317 lines
9.0 KiB
TypeScript
317 lines
9.0 KiB
TypeScript
"use client";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
|
|
import { Controller, useForm } from "react-hook-form";
|
|
import z from "zod";
|
|
|
|
import { Price, PriceValue } from "@/components/shadcnblocks/price";
|
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Field, FieldLabel } from "@/components/ui/field";
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
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 FieldName = keyof FormType;
|
|
|
|
type FormType = z.infer<typeof formSchema>;
|
|
|
|
type Image = {
|
|
src: string;
|
|
alt: string;
|
|
};
|
|
|
|
type Option = {
|
|
id: string;
|
|
label: string;
|
|
color: string;
|
|
value: string;
|
|
image: Image;
|
|
};
|
|
|
|
interface ProductFormProps {
|
|
colorHinges?: {
|
|
name: FieldName;
|
|
options: Omit<Option, "image">[];
|
|
};
|
|
form: UseFormReturn<FormType>;
|
|
}
|
|
|
|
interface RadioGroupProps {
|
|
options?: Omit<Option, "image">[];
|
|
field: ControllerRenderProps<FormType>;
|
|
}
|
|
|
|
type ColorOptionProps = Omit<Option, "image">;
|
|
|
|
interface ProductPrice {
|
|
regular: number;
|
|
sale?: number;
|
|
currency: string;
|
|
}
|
|
|
|
interface Product {
|
|
collection: {
|
|
label: string;
|
|
link: string;
|
|
};
|
|
name: string;
|
|
variantId: string;
|
|
link: string;
|
|
image: Image;
|
|
price: ProductPrice;
|
|
variants: {
|
|
name: FieldName;
|
|
options: Array<Option>;
|
|
};
|
|
quickAdd: boolean;
|
|
stockStatusCode: StockStatusCode;
|
|
}
|
|
|
|
const PRODUCT_CARD: Product = {
|
|
collection: {
|
|
label: "رنگهای انتزاعی",
|
|
link: "#",
|
|
},
|
|
name: "قاب میدنایت استریک",
|
|
link: "#",
|
|
image: {
|
|
src: "/block-media/shadcnblocks/block/ecommerce/other/jascent-leung-CubSQS4iYEE-unsplash-3.jpg",
|
|
alt: "قاب موبایل میدنایت استریک",
|
|
},
|
|
price: {
|
|
regular: 590000.0,
|
|
sale: 490000.0,
|
|
currency: "ریال",
|
|
},
|
|
variants: {
|
|
name: "color",
|
|
options: [
|
|
{
|
|
id: "midnight_black-1",
|
|
label: "مشکی نیمهشب",
|
|
color: "#0B0B0B",
|
|
value: "midnight_black",
|
|
image: {
|
|
src: "/block-media/shadcnblocks/block/ecommerce/other/jascent-leung-CubSQS4iYEE-unsplash-3.jpg",
|
|
alt: "قاب موبایل مشکی نیمهشب",
|
|
},
|
|
},
|
|
{
|
|
id: "royal_indigo-1",
|
|
label: "نیلی سلطنتی",
|
|
color: "#3B0A45",
|
|
value: "royal_indigo",
|
|
image: {
|
|
src: "/block-media/shadcnblocks/block/ecommerce/other/jascent-leung-wyEh0ZnwUFM-unsplash-3.jpg",
|
|
alt: "قاب موبایل نیلی سلطنتی",
|
|
},
|
|
},
|
|
{
|
|
id: "vibrant_tangerine-1",
|
|
label: "نارنجی درخشان",
|
|
color: "#FF6400",
|
|
value: "vibrant_tangerine",
|
|
image: {
|
|
src: "/block-media/shadcnblocks/block/ecommerce/other/jascent-leung-kfv-hU9ZVkk-unsplash-3.jpg",
|
|
alt: "قاب موبایل نارنجی درخشان",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
quickAdd: true,
|
|
variantId: "midnight_black",
|
|
stockStatusCode: "IN_STOCK",
|
|
};
|
|
|
|
const formSchema = z.object({
|
|
color: z.string(),
|
|
});
|
|
|
|
interface ProductCard6Props {
|
|
className?: string;
|
|
}
|
|
|
|
const ProductCard6 = ({ className }: ProductCard6Props) => {
|
|
const { regular, sale, currency } = PRODUCT_CARD.price;
|
|
|
|
const form = useForm<FormType>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
color: PRODUCT_CARD.variantId,
|
|
},
|
|
});
|
|
const selectedVariant = form.watch("color");
|
|
const currentVariantImage = PRODUCT_CARD.variants.options.find(
|
|
(item) => item.value === selectedVariant,
|
|
)?.image;
|
|
|
|
function onSubmit(values: FormType) {
|
|
console.log(values);
|
|
}
|
|
|
|
return (
|
|
<div className={cn("flex w-full justify-center", className)}>
|
|
<Card className="group max-w-72 gap-2 rounded-none border-none bg-background p-0 shadow-none">
|
|
<CardHeader className="gap-0 p-0">
|
|
<AspectRatio ratio={0.76984127} className="overflow-hidden">
|
|
<a href={PRODUCT_CARD.link} className="block size-full">
|
|
{currentVariantImage && (
|
|
<img
|
|
src={currentVariantImage?.src}
|
|
alt={currentVariantImage?.alt}
|
|
className="block size-full object-cover object-center transition-transform duration-500 group-hover:scale-110"
|
|
/>
|
|
)}
|
|
</a>
|
|
{PRODUCT_CARD.quickAdd &&
|
|
PRODUCT_CARD.stockStatusCode === STOCK_STATUS.IN_STOCK && (
|
|
<div className="absolute inset-x-5 bottom-5 transition-opacity duration-400 group-hover:opacity-100 xl:opacity-0">
|
|
<Button
|
|
onClick={() => {
|
|
form.trigger().then((valid) => {
|
|
if (valid) {
|
|
form.handleSubmit(onSubmit)();
|
|
}
|
|
});
|
|
}}
|
|
className="w-full"
|
|
>
|
|
افزودن سریع
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</AspectRatio>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
{(PRODUCT_CARD.stockStatusCode || PRODUCT_CARD.price?.sale) && (
|
|
<div className="text-sm">
|
|
{PRODUCT_CARD.stockStatusCode === STOCK_STATUS.OUT_OF_STOCK ? (
|
|
<span className="text-muted-foreground">فروخته شد</span>
|
|
) : (
|
|
PRODUCT_CARD.price.sale && (
|
|
<span className="text-destructive">حراج</span>
|
|
)
|
|
)}
|
|
</div>
|
|
)}
|
|
<div
|
|
className={`pt-0.5 ${PRODUCT_CARD.stockStatusCode === STOCK_STATUS.IN_STOCK && !PRODUCT_CARD.price?.sale && "mt-5"}`}
|
|
>
|
|
<a className="text-sm font-bold" href={PRODUCT_CARD.collection.link}>
|
|
{PRODUCT_CARD.collection.label}
|
|
</a>
|
|
<div className="flex flex-wrap items-center gap-2 xl:flex-nowrap">
|
|
<CardTitle className="text-sm font-normal">
|
|
{PRODUCT_CARD.name}
|
|
</CardTitle>
|
|
<div className="transition-opacity duration-400 group-hover:opacity-100 xl:opacity-0">
|
|
<ProductForm colorHinges={PRODUCT_CARD.variants} form={form} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter className="relative mt-1 flex-col items-start gap-3 p-0 xl:flex-row">
|
|
<div className="transition-opacity duration-400 xl:group-hover:opacity-0">
|
|
<Price onSale={sale != null} className="text-sm font-medium">
|
|
<PriceValue price={sale} currency={currency} variant="sale" />
|
|
<PriceValue price={regular} currency={currency} variant="regular" />
|
|
</Price>
|
|
</div>
|
|
<div className="absolute flex items-center gap-3 bg-background opacity-0 transition-opacity duration-400 group-hover:opacity-100">
|
|
<Button variant="link" className="h-fit p-0 underline">
|
|
همین الان بخرید
|
|
</Button>
|
|
<Button
|
|
variant="link"
|
|
className="h-fit p-0 text-muted-foreground underline"
|
|
>
|
|
مشاهده سریع
|
|
</Button>
|
|
</div>
|
|
</CardFooter>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ProductForm = ({ colorHinges, form }: ProductFormProps) => {
|
|
if (!colorHinges) return;
|
|
|
|
return (
|
|
<form>
|
|
<Controller
|
|
control={form.control}
|
|
name={colorHinges.name}
|
|
render={({ field }) => (
|
|
<Field>
|
|
<ColorRadioGroup field={field} options={colorHinges.options} />
|
|
</Field>
|
|
)}
|
|
/>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
const ColorRadioGroup = ({ options, field }: RadioGroupProps) => {
|
|
if (!options) return;
|
|
|
|
return (
|
|
<RadioGroup
|
|
{...field}
|
|
value={`${field.value}`}
|
|
onValueChange={(value) => {
|
|
if (value != field.value && value) {
|
|
field.onChange(value);
|
|
}
|
|
}}
|
|
className="flex gap-0"
|
|
>
|
|
{options &&
|
|
options.map((item, index) => (
|
|
<Field key={`product-list-6-color-input-${index}`}>
|
|
<ColorOption
|
|
label={item.label}
|
|
id={item.id}
|
|
color={item.color}
|
|
value={item.value}
|
|
/>
|
|
</Field>
|
|
))}
|
|
</RadioGroup>
|
|
);
|
|
};
|
|
|
|
const ColorOption = ({ id, value, color, label }: ColorOptionProps) => {
|
|
return (
|
|
<FieldLabel
|
|
htmlFor={id}
|
|
aria-label={label}
|
|
className="relative flex size-5 cursor-pointer after:pointer-events-none after:absolute after:inset-1/2 after:size-1 after:-translate-1/2 after:rounded-full after:bg-white after:opacity-0 after:transition-opacity has-checked:after:opacity-100"
|
|
>
|
|
<RadioGroupItem id={id} className="sr-only" value={value} />
|
|
<div
|
|
style={{
|
|
backgroundColor: color,
|
|
}}
|
|
className="m-auto size-2.5 rounded-full"
|
|
></div>
|
|
</FieldLabel>
|
|
);
|
|
};
|
|
|
|
export { ProductCard6 };
|