331 lines
11 KiB
TypeScript
331 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { ShoppingBag } from "lucide-react";
|
|
import { startTransition, useEffect, useMemo, useState } from "react";
|
|
|
|
import { Price, PriceValue } from "@/components/shadcnblocks/price";
|
|
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 { Label } from "@/components/ui/label";
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface ProductPrice {
|
|
regular: number;
|
|
sale?: number;
|
|
currency: string;
|
|
}
|
|
|
|
type Badge = {
|
|
text: string;
|
|
color: string;
|
|
};
|
|
|
|
type Image = {
|
|
srcset?: string;
|
|
src: string;
|
|
sizes?: string;
|
|
alt: string;
|
|
};
|
|
|
|
type Variant = {
|
|
color: string;
|
|
value: string;
|
|
id: string;
|
|
label: string;
|
|
link: string;
|
|
images: Array<Image>;
|
|
};
|
|
|
|
interface ProductColorsProps {
|
|
variants: Omit<Variant, "images">[];
|
|
value: string;
|
|
onOptionHover: (value: string) => void;
|
|
}
|
|
|
|
interface BadgesProps {
|
|
badges?: Array<Badge>;
|
|
}
|
|
|
|
interface Product {
|
|
name: string;
|
|
price: ProductPrice;
|
|
category: {
|
|
label: string;
|
|
link: string;
|
|
};
|
|
link: string;
|
|
badges?: Array<Badge>;
|
|
variants: Array<Variant>;
|
|
images: Array<Image>;
|
|
}
|
|
|
|
const PRODUCT_CARD: Product = {
|
|
name: "لیوان سرامیکی",
|
|
price: {
|
|
regular: 190000.0,
|
|
sale: 150000.0,
|
|
currency: "ریال",
|
|
},
|
|
category: {
|
|
label: "لیوانها",
|
|
link: "#",
|
|
},
|
|
link: "#",
|
|
badges: [
|
|
{
|
|
text: "بهترین انتخاب!",
|
|
color: "oklch(54.6% 0.245 262.881)",
|
|
},
|
|
{
|
|
text: "حراج",
|
|
color: "oklch(57.7% 0.245 27.325)",
|
|
},
|
|
],
|
|
variants: [
|
|
{
|
|
color: "#A8A081",
|
|
link: "#",
|
|
value: "light_brown",
|
|
id: "light_brown",
|
|
label: "قهوهای روشن",
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-1.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-1.jpg 640w",
|
|
alt: "لیوان سرامیکی قهوهای روشن",
|
|
sizes:
|
|
"(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-1.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-1.jpg 640w",
|
|
alt: "لیوان سرامیکی نمای دوم",
|
|
sizes:
|
|
"(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
color: "#C2CCCD",
|
|
link: "#",
|
|
value: "light_blue",
|
|
id: "light_blue",
|
|
label: "آبی روشن",
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-1.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-1.jpg 640w",
|
|
alt: "لیوان سرامیکی آبی روشن",
|
|
sizes:
|
|
"(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-1.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-1.jpg 640w",
|
|
alt: "لیوان سرامیکی نمای دوم",
|
|
sizes:
|
|
"(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-1.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-bWlbcvFMUh4-unsplash-1.jpg 640w",
|
|
alt: "لیوان سرامیکی",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-1.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/other/cafeconcetto-L2r9L6Llyp4-unsplash-1.jpg 640w",
|
|
alt: "لیوان سرامیکی نمای دوم",
|
|
sizes: "(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
};
|
|
|
|
interface ProductCard9Props {
|
|
className?: string;
|
|
}
|
|
|
|
const ProductCard9 = ({ className }: ProductCard9Props) => {
|
|
const { regular, sale, currency } = PRODUCT_CARD.price;
|
|
const [selectedColor, setSelectedColor] = useState<string>("");
|
|
const [productImages, setProductImages] = useState(PRODUCT_CARD.images);
|
|
|
|
const onOptionHover = (value: string) => {
|
|
setSelectedColor(value);
|
|
};
|
|
|
|
const selectedVariantImages = useMemo(() => {
|
|
return (
|
|
PRODUCT_CARD.variants.find((v) => v.value === selectedColor)?.images ??
|
|
PRODUCT_CARD.images
|
|
);
|
|
}, [selectedColor]);
|
|
|
|
useEffect(() => {
|
|
startTransition(() => {
|
|
setProductImages(selectedVariantImages);
|
|
});
|
|
}, [selectedColor, selectedVariantImages]);
|
|
|
|
return (
|
|
<div className={cn("flex w-full justify-center", className)}>
|
|
<Card className="relative block w-full max-w-64 rounded-none border-none bg-background p-0 shadow-none">
|
|
<CardContent className="p-0">
|
|
<div className="group relative overflow-hidden">
|
|
<a href={PRODUCT_CARD.link}>
|
|
<AspectRatio
|
|
ratio={1}
|
|
className="flex items-center justify-center overflow-hidden rounded-xl bg-muted/20"
|
|
>
|
|
{productImages.map((img, index) => (
|
|
<div
|
|
className="absolute inset-0 size-full transition-opacity duration-700 first:z-20 group-hover:first:opacity-0 last:z-10 first:[&>img]:scale-105 first:[&>img]:delay-50 group-hover:first:[&>img]:scale-100 last:[&>img]:scale-105 group-hover:last:[&>img]:scale-100"
|
|
key={`product-list-9-card-img-wrapper-${index}`}
|
|
>
|
|
<img
|
|
srcSet={img.srcset}
|
|
src={img.src}
|
|
alt={img.alt}
|
|
sizes={img.sizes}
|
|
className="size-full object-contain object-center transition-transform duration-700"
|
|
/>
|
|
</div>
|
|
))}
|
|
</AspectRatio>
|
|
</a>
|
|
<div className="absolute start-2.5 top-2.5 z-60">
|
|
<Badges badges={PRODUCT_CARD.badges} />
|
|
</div>
|
|
<div className="absolute end-2.5 bottom-2.5 z-60 md:hidden">
|
|
<Button variant="secondary" size="icon" className="rounded-full">
|
|
<ShoppingBag />
|
|
</Button>
|
|
</div>
|
|
<div className="absolute inset-x-4 bottom-4 z-60 hidden md:block">
|
|
<Button className="w-full rounded-full opacity-0 duration-700 lg:translate-y-4 lg:group-hover:translate-y-0 lg:group-hover:opacity-100">
|
|
افزودن به سبد
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="flex min-h-44 flex-col gap-2 px-5 pb-8 pt-6 lg:px-7 lg:pb-8">
|
|
<a
|
|
href={PRODUCT_CARD.category.link}
|
|
className="text-xs leading-loose font-bold text-muted-foreground uppercase underline-offset-4 hover:underline"
|
|
>
|
|
{PRODUCT_CARD.category.label}
|
|
</a>
|
|
<a
|
|
href={PRODUCT_CARD.link}
|
|
className="underline-offset-4 hover:underline"
|
|
>
|
|
<CardTitle className="text-xl leading-none font-medium">
|
|
{PRODUCT_CARD.name}
|
|
</CardTitle>
|
|
</a>
|
|
<Price
|
|
onSale={sale != null}
|
|
className="text-sm leading-normal font-bold"
|
|
>
|
|
<PriceValue
|
|
price={sale}
|
|
currency={currency}
|
|
variant="sale"
|
|
className="text-red-700"
|
|
/>
|
|
<PriceValue price={regular} currency={currency} variant="regular" />
|
|
</Price>
|
|
<ProductColors
|
|
value={selectedColor}
|
|
onOptionHover={onOptionHover}
|
|
variants={PRODUCT_CARD.variants}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ProductColors = ({
|
|
value,
|
|
onOptionHover,
|
|
variants,
|
|
}: ProductColorsProps) => {
|
|
if (!variants) return;
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<RadioGroup value={value} className="mt-2 flex items-center gap-2">
|
|
{variants.map((item, index) => (
|
|
<Label
|
|
className="relative block pb-1.5 after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:w-full after:bg-primary after:opacity-0 after:transition-opacity hover:after:opacity-100 has-data-[state=checked]:after:opacity-100"
|
|
htmlFor={item.id}
|
|
key={`product-list-9-color-${index}`}
|
|
>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<a href={item.link}>
|
|
<div
|
|
style={{
|
|
backgroundColor: item.color,
|
|
}}
|
|
className="size-5.5 border"
|
|
onMouseEnter={() => onOptionHover(item.value)}
|
|
></div>
|
|
</a>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>{item.label}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<RadioGroupItem
|
|
value={item.value}
|
|
id={item.id}
|
|
className="sr-only"
|
|
/>
|
|
</Label>
|
|
))}
|
|
</RadioGroup>
|
|
</TooltipProvider>
|
|
);
|
|
};
|
|
|
|
const Badges = ({ badges }: BadgesProps) => {
|
|
if (!badges) return;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1.5">
|
|
{badges.map((item, index) => (
|
|
<Badge
|
|
style={{ backgroundColor: item.color }}
|
|
key={`product-list-9-badge-${index}`}
|
|
className="rounded-full dark:text-white"
|
|
>
|
|
{item.text}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export { ProductCard9 };
|