461 lines
17 KiB
TypeScript
461 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import Autoplay from "embla-carousel-autoplay";
|
|
import { ChevronLeft, ChevronRight, Heart, ShoppingBag } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
|
|
import {
|
|
Price,
|
|
type PriceType,
|
|
PriceValue,
|
|
} from "@/components/shadcnblocks/price";
|
|
import { Rating } from "@/components/shadcnblocks/rating";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardAction, CardContent, CardTitle } from "@/components/ui/card";
|
|
import type { CarouselApi } from "@/components/ui/carousel";
|
|
import {
|
|
Carousel,
|
|
CarouselContent,
|
|
CarouselItem,
|
|
} from "@/components/ui/carousel";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type Image = {
|
|
src: string;
|
|
srcset?: string;
|
|
sizes?: string;
|
|
alt: string;
|
|
};
|
|
|
|
interface Reviews {
|
|
rate: number;
|
|
totalReviewers: string;
|
|
}
|
|
|
|
type Product = {
|
|
name: string;
|
|
category: {
|
|
label: string;
|
|
link: string;
|
|
};
|
|
images: Array<Image>;
|
|
link: string;
|
|
price: PriceType;
|
|
reviews: Reviews;
|
|
badges?: Array<string>;
|
|
};
|
|
|
|
type ProductCardProps = Product;
|
|
|
|
interface HeroCarouselItem {
|
|
media: {
|
|
type: "image" | "video";
|
|
src: string;
|
|
alt?: string;
|
|
};
|
|
title?: string;
|
|
description?: string;
|
|
cta?: {
|
|
label: string;
|
|
href: string;
|
|
};
|
|
product?: Product;
|
|
}
|
|
|
|
interface EcommerceHero7Props {
|
|
className?: string;
|
|
carouselItems: HeroCarouselItem[];
|
|
}
|
|
const HERO_CAROUSEL: HeroCarouselItem[] = [
|
|
{
|
|
title: "آبرسانی عمیق، درخشش محسوس",
|
|
description:
|
|
"کرمی سبک روزانه که رطوبت را بازمیگرداند، بافت پوست را نرم میکند و درخشش طبیعی به پوست میبخشد.",
|
|
media: {
|
|
type: "video",
|
|
src: "/block-media/shadcnblocks/block/4153586-hd_1366_720_50fps.mp4",
|
|
},
|
|
cta: {
|
|
label: "خرید کنید",
|
|
href: "#",
|
|
},
|
|
product: {
|
|
name: "کرم آبرسان لوماگلو",
|
|
category: {
|
|
label: "کرم صورت",
|
|
link: "#",
|
|
},
|
|
badges: ["پرفروش", "جدید"],
|
|
reviews: {
|
|
rate: 4.5,
|
|
totalReviewers: "۵٫۸ هزار",
|
|
},
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/skin-care-product/point-normal-Xp0bTCLD07k-unsplash-3.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/skin-care-product/point-normal-Xp0bTCLD07k-unsplash-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/point-normal-Xp0bTCLD07k-unsplash-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/point-normal-Xp0bTCLD07k-unsplash-1.jpg 640w",
|
|
alt: "کرم آبرسان صورت در بستهبندی مینیمال",
|
|
sizes:
|
|
"(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/skin-care-product/onela-ymeri-vaQgJjd9TUY-unsplash-3.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/skin-care-product/onela-ymeri-vaQgJjd9TUY-unsplash-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/onela-ymeri-vaQgJjd9TUY-unsplash-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/onela-ymeri-vaQgJjd9TUY-unsplash-1.jpg 640w",
|
|
alt: "جزئیات بافت و استفاده از کرم",
|
|
sizes:
|
|
"(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
link: "#",
|
|
price: {
|
|
regular: 49.0,
|
|
currency: "IRR",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "روتین کامل درخشندگی شما",
|
|
description:
|
|
"ست مراقبت پوستی منتخب برای پاکسازی، آبرسانی و بازسازی؛ برای پوستی متعادل و درخشان.",
|
|
media: {
|
|
type: "image",
|
|
src: "/block-media/shadcnblocks/block/ecommerce/skin-care-product/pexels-amelie-chen-243775000-12352170-3.jpg",
|
|
alt: "ست کامل مراقبت پوست",
|
|
},
|
|
cta: {
|
|
label: "خرید کنید",
|
|
href: "#",
|
|
},
|
|
product: {
|
|
name: "ست روتین درخشندگی",
|
|
category: {
|
|
label: "ست مراقبت پوست",
|
|
link: "#",
|
|
},
|
|
badges: ["پرفروش", "جدید"],
|
|
reviews: {
|
|
rate: 5,
|
|
totalReviewers: "۵٫۸ هزار",
|
|
},
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/skin-care-product/pexels-amelie-chen-243775000-12352170-3.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/skin-care-product/pexels-amelie-chen-243775000-12352170-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/pexels-amelie-chen-243775000-12352170-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/pexels-amelie-chen-243775000-12352170-1.jpg 640w",
|
|
alt: "ست کامل مراقبت پوست روی پسزمینه خنثی",
|
|
sizes:
|
|
"(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/skin-care-product/ibnu-ihza-Z7u2bpbE65Q-unsplash-3.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/skin-care-product/ibnu-ihza-Z7u2bpbE65Q-unsplash-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/ibnu-ihza-Z7u2bpbE65Q-unsplash-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/ibnu-ihza-Z7u2bpbE65Q-unsplash-1.jpg 640w",
|
|
alt: "بطریهای مراقبت پوست برای روتین روزانه",
|
|
sizes:
|
|
"(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
link: "#",
|
|
price: {
|
|
regular: 499.0,
|
|
sale: 389.0,
|
|
currency: "IRR",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "درخشش را حفظ کنید",
|
|
description:
|
|
"اسپری تازهکنندهای که رطوبت را قفل میکند، درخشش را تقویت میکند و پوست را تمام روز شاداب نگه میدارد.",
|
|
media: {
|
|
type: "image",
|
|
src: "/block-media/shadcnblocks/block/ecommerce/skin-care-product/pexels-rdne-8903695-3.jpg",
|
|
alt: "اسپری صورت برای آبرسانی",
|
|
},
|
|
cta: {
|
|
label: "خرید کنید",
|
|
href: "#",
|
|
},
|
|
product: {
|
|
name: "اسپری حفظ درخشندگی",
|
|
category: {
|
|
label: "اسپری صورت",
|
|
link: "#",
|
|
},
|
|
badges: ["جدید"],
|
|
reviews: {
|
|
rate: 5,
|
|
totalReviewers: "۵٫۸ هزار",
|
|
},
|
|
images: [
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/skin-care-product/pexels-rdne-8903695-3.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/skin-care-product/pexels-rdne-8903695-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/pexels-rdne-8903695-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/pexels-rdne-8903695-1.jpg 640w",
|
|
alt: "استفاده از اسپری صورت برای آبرسانی",
|
|
sizes:
|
|
"(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
{
|
|
src: "/block-media/shadcnblocks/block/ecommerce/skin-care-product/nora-hutton-AjU6Z5k_uBI-unsplash-3.jpg",
|
|
srcset:
|
|
"/block-media/shadcnblocks/block/ecommerce/skin-care-product/nora-hutton-AjU6Z5k_uBI-unsplash-3.jpg 1920w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/nora-hutton-AjU6Z5k_uBI-unsplash-2.jpg 1280w, /block-media/shadcnblocks/block/ecommerce/skin-care-product/nora-hutton-AjU6Z5k_uBI-unsplash-1.jpg 640w",
|
|
alt: "بطری اسپری در نور طبیعی ملایم",
|
|
sizes:
|
|
"(min-width: 1920px) 1920px, (min-width: 1280px) 1280px, 100vw",
|
|
},
|
|
],
|
|
link: "#",
|
|
price: {
|
|
regular: 69.0,
|
|
currency: "IRR",
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
const EcommerceHero7 = ({
|
|
className,
|
|
carouselItems = HERO_CAROUSEL,
|
|
}: EcommerceHero7Props) => {
|
|
const [api, setApi] = useState<CarouselApi>();
|
|
const [current, setCurrent] = useState(1);
|
|
|
|
useEffect(() => {
|
|
if (!api) return;
|
|
|
|
const updateCurrent = (api: CarouselApi) => {
|
|
if (!api) return;
|
|
setCurrent(api?.selectedScrollSnap());
|
|
};
|
|
api.on("reInit", updateCurrent).on("select", updateCurrent);
|
|
|
|
return () => {
|
|
api.off("reInit", updateCurrent).off("select", updateCurrent);
|
|
};
|
|
}, [api]);
|
|
|
|
return (
|
|
<header dir="rtl" lang="fa" className={cn("", className)}>
|
|
<div className="relative">
|
|
<Carousel
|
|
opts={{
|
|
loop: true,
|
|
}}
|
|
plugins={[
|
|
Autoplay({
|
|
delay: 6000,
|
|
}),
|
|
]}
|
|
setApi={setApi}
|
|
>
|
|
<CarouselContent className="m-0">
|
|
{carouselItems.map(
|
|
({ title, description, media, product, cta }, index) => (
|
|
<CarouselItem className="group h-dvh min-h-170 p-0" key={index}>
|
|
<div className="relative flex size-full flex-col items-center justify-end px-4 py-25 after:absolute after:inset-0 after:block after:bg-black/20 lg:px-40">
|
|
<div
|
|
className={cn(
|
|
"relative z-10 w-full",
|
|
"flex flex-col gap-13 md:flex-row",
|
|
"md:items-end md:group-odd:flex-row-reverse",
|
|
)}
|
|
>
|
|
<div className="flex flex-1 flex-col gap-4 text-start md:group-even:items-end md:group-even:justify-end">
|
|
<h1 className="font-serif text-2xll text-white md:text-2xll md:group-even:text-start lg:text-2xll">
|
|
{title}
|
|
</h1>
|
|
<p className="max-w-130 text-lg text-white md:group-even:text-start">
|
|
{description}
|
|
</p>
|
|
{cta && (
|
|
<Button
|
|
asChild
|
|
variant="secondary"
|
|
className="mt-1 self-start rounded-full md:group-even:self-end"
|
|
>
|
|
<a href={cta.href}>{cta.label}</a>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
{product && (
|
|
<div className="md:basis-80">
|
|
<ProductCard {...product} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="absolute inset-0">
|
|
{media.type === "image" ? (
|
|
<img
|
|
src={media.src}
|
|
alt={media.alt}
|
|
className="block size-full object-cover object-center"
|
|
/>
|
|
) : (
|
|
<video
|
|
loop
|
|
muted
|
|
autoPlay
|
|
src={media.src}
|
|
className="block size-full object-cover object-center"
|
|
></video>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CarouselItem>
|
|
),
|
|
)}
|
|
</CarouselContent>
|
|
<div className="pointer-events-none absolute bottom-12.5 contents flex w-full translate-y-1/2 items-center justify-between px-2 lg:top-1/2 lg:-translate-y-1/2 lg:px-12">
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="pointer-events-auto size-12 rounded-full text-white"
|
|
onClick={() => api?.scrollPrev()}
|
|
aria-label="اسلاید قبلی"
|
|
>
|
|
<ChevronLeft className="size-11 stroke-1 rtl:rotate-180" />
|
|
</Button>
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
onClick={() => api?.scrollNext()}
|
|
className="pointer-events-auto size-12 rounded-full text-white"
|
|
aria-label="اسلاید بعدی"
|
|
>
|
|
<ChevronRight className="size-11 stroke-1 rtl:rotate-180" />
|
|
</Button>
|
|
</div>
|
|
</Carousel>
|
|
<div className="absolute inset-x-0 bottom-12.5 translate-y-1/2">
|
|
<ol className="flex items-center justify-center gap-3">
|
|
{api?.scrollSnapList().map((_, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => api?.scrollTo(index)}
|
|
data-state={current === index ? "active" : "inactive"}
|
|
aria-label={`رفتن به اسلاید ${(index + 1).toLocaleString("fa-IR")}`}
|
|
className="block size-2.5 rounded-full border border-primary data-[state=active]:bg-primary"
|
|
></button>
|
|
))}
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
};
|
|
|
|
const ProductCard = ({
|
|
images,
|
|
link,
|
|
name,
|
|
category,
|
|
price,
|
|
badges,
|
|
reviews,
|
|
}: ProductCardProps) => {
|
|
const { regular, sale, currency } = price;
|
|
|
|
return (
|
|
<Card className="group w-full rounded-none border-none bg-background p-0 shadow-none md:max-w-[57.5rem]">
|
|
<CardContent className="flex flex-row p-0 md:flex-col">
|
|
<div className="relative basis-30 self-stretch overflow-hidden md:basis-auto">
|
|
<div className="absolute inset-x-0 top-0 z-20 max-md:hidden">
|
|
{badges && (
|
|
<ul className="flex flex-col gap-2 p-4">
|
|
{badges.map((text, index) => (
|
|
<Badge
|
|
variant="secondary"
|
|
className="uppercase"
|
|
key={`product-list-13-card-badge-${index}`}
|
|
>
|
|
{text}
|
|
</Badge>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
<div className="size-full overflow-hidden md:aspect-[0.8]">
|
|
{images.map((img, index) => (
|
|
<img
|
|
key={`product-list-13-card-img-${index}`}
|
|
srcSet={img.srcset}
|
|
src={img.src}
|
|
sizes={img.sizes}
|
|
alt={img.alt}
|
|
className="absolute inset-0 size-full origin-center object-cover object-center transition-all duration-500 group-hover:scale-105 first:opacity-100 group-hover:first:not-only:opacity-0 nth-2:opacity-0 group-hover:nth-2:opacity-100"
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className="absolute inset-x-0 bottom-0 z-10 overflow-hidden max-md:hidden">
|
|
<div className="flex gap-4 p-4">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="rounded-full duration-350 lg:translate-y-[calc(100%+1rem)] lg:transition-transform lg:group-hover:translate-y-0"
|
|
aria-label="افزودن به سبد خرید"
|
|
>
|
|
<ShoppingBag />
|
|
</Button>
|
|
<CardAction className="flex-1 translate-y-[calc(100%+1rem)] transition-transform duration-350 group-hover:translate-y-0">
|
|
<Button
|
|
size="lg"
|
|
variant="secondary"
|
|
className="w-full rounded-full"
|
|
>
|
|
مشاهده سریع
|
|
</Button>
|
|
</CardAction>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="rounded-full"
|
|
aria-label="افزودن به علاقهمندیها"
|
|
>
|
|
<Heart />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="px-2 py-4 text-start">
|
|
<a
|
|
href={category.link}
|
|
className="mb-2 text-sm leading-normal font-light uppercase"
|
|
>
|
|
{category.label}
|
|
</a>
|
|
<div className="flex items-start justify-between gap-4 max-md:flex-col">
|
|
<CardTitle className="flex-1">
|
|
<a href={link} className="leading-normal font-medium">
|
|
{name}
|
|
</a>
|
|
</CardTitle>
|
|
<Price onSale={sale != null} className="gap-1 text-lg">
|
|
<PriceValue price={sale} currency={currency} variant="sale" />
|
|
<PriceValue
|
|
price={regular}
|
|
currency={currency}
|
|
variant="regular"
|
|
/>
|
|
</Price>
|
|
</div>
|
|
<div className="mt-1.5 flex flex-wrap items-center gap-2.5">
|
|
<Rating
|
|
className="[&_svg]:size-3.5 [&>div]:size-3.5"
|
|
rate={reviews.rate}
|
|
/>
|
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
{reviews.totalReviewers} نظر
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export { EcommerceHero7 };
|