Wire homepage images to Meshkee slots and publish the catalog.
The dashboard can refresh these keys from the live site. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
1272977923
commit
1c2d1eb31c
@@ -1,8 +1,14 @@
|
||||
import { Placeholder } from "@/components/ui/Placeholder";
|
||||
import { SectionTitle } from "@/components/ui/SectionTitle";
|
||||
import { SlotImage } from "@/components/ui/SlotImage";
|
||||
import { categories } from "@/data/sample";
|
||||
import type { StaticImageWithUrl } from "@/lib/meshkee";
|
||||
|
||||
export function FeaturedCategories() {
|
||||
type FeaturedCategoriesProps = {
|
||||
images?: StaticImageWithUrl[];
|
||||
};
|
||||
|
||||
export function FeaturedCategories({ images = [] }: FeaturedCategoriesProps) {
|
||||
return (
|
||||
<section className="py-10 md:py-14">
|
||||
<div className="mx-auto max-w-7xl px-4">
|
||||
@@ -12,27 +18,47 @@ export function FeaturedCategories() {
|
||||
action="همه دستهها"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-8">
|
||||
{categories.map((cat, i) => (
|
||||
<a
|
||||
key={cat.id}
|
||||
href="#"
|
||||
className="animate-slide-in group flex flex-col items-center rounded-xl border border-border bg-surface p-3 transition-all hover:border-red hover:shadow-md"
|
||||
style={{ animationDelay: `${i * 60}ms` }}
|
||||
>
|
||||
<Placeholder
|
||||
src={cat.image}
|
||||
alt={cat.name}
|
||||
width="72px"
|
||||
height="72px"
|
||||
label="72×72"
|
||||
className="mb-3 rounded-full transition-colors group-hover:border-red"
|
||||
/>
|
||||
<span className="text-center text-xs font-medium leading-relaxed md:text-sm">
|
||||
{cat.name}
|
||||
</span>
|
||||
<span className="mt-1 text-[11px] text-muted">{cat.count} محصول</span>
|
||||
</a>
|
||||
))}
|
||||
{images.length > 0
|
||||
? images.map((image, i) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="animate-slide-in group flex flex-col items-center rounded-xl border border-border bg-surface p-3 transition-all hover:border-red hover:shadow-md"
|
||||
style={{ animationDelay: `${i * 60}ms` }}
|
||||
>
|
||||
<SlotImage
|
||||
url={image.url}
|
||||
title={image.title}
|
||||
linkUrl={image.linkUrl}
|
||||
imgClassName="mb-3 h-[72px] w-[72px] rounded-full object-cover transition-colors group-hover:border-red"
|
||||
/>
|
||||
{image.title ? (
|
||||
<span className="text-center text-xs font-medium leading-relaxed md:text-sm">
|
||||
{image.title}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
))
|
||||
: categories.map((cat, i) => (
|
||||
<a
|
||||
key={cat.id}
|
||||
href="#"
|
||||
className="animate-slide-in group flex flex-col items-center rounded-xl border border-border bg-surface p-3 transition-all hover:border-red hover:shadow-md"
|
||||
style={{ animationDelay: `${i * 60}ms` }}
|
||||
>
|
||||
<Placeholder
|
||||
src={cat.image}
|
||||
alt={cat.name}
|
||||
width="72px"
|
||||
height="72px"
|
||||
label="72×72"
|
||||
className="mb-3 rounded-full transition-colors group-hover:border-red"
|
||||
/>
|
||||
<span className="text-center text-xs font-medium leading-relaxed md:text-sm">
|
||||
{cat.name}
|
||||
</span>
|
||||
<span className="mt-1 text-[11px] text-muted">{cat.count} محصول</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2,15 +2,34 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Placeholder } from "@/components/ui/Placeholder";
|
||||
import { SlotImage } from "@/components/ui/SlotImage";
|
||||
import { sliderSlides } from "@/data/sample";
|
||||
import type { StaticImageWithUrl } from "@/lib/meshkee";
|
||||
|
||||
export function HeroSlider() {
|
||||
const [active, setActive] = useState(1);
|
||||
type HeroSliderProps = {
|
||||
images?: StaticImageWithUrl[];
|
||||
};
|
||||
|
||||
const getIndex = (offset: number) => {
|
||||
const len = sliderSlides.length;
|
||||
return (active + offset + len) % len;
|
||||
};
|
||||
export function HeroSlider({ images = [] }: HeroSliderProps) {
|
||||
const remote = images.length > 0;
|
||||
const slides = remote
|
||||
? images.map((image) => ({
|
||||
id: image.id,
|
||||
title: image.title ?? "",
|
||||
src: image.url,
|
||||
linkUrl: image.linkUrl,
|
||||
}))
|
||||
: sliderSlides.map((slide) => ({
|
||||
id: String(slide.id),
|
||||
title: slide.title,
|
||||
src: slide.image,
|
||||
linkUrl: undefined as string | null | undefined,
|
||||
}));
|
||||
|
||||
const [active, setActive] = useState(0);
|
||||
const len = slides.length;
|
||||
|
||||
const getIndex = (offset: number) => (active + offset + len) % len;
|
||||
|
||||
const positions = [
|
||||
{ offset: -1, scale: "scale-[0.92]", opacity: "opacity-55", z: "z-10" },
|
||||
@@ -24,7 +43,30 @@ export function HeroSlider() {
|
||||
<div className="relative h-[220px] sm:h-[280px] md:h-[340px] lg:h-[380px]">
|
||||
{positions.map(({ offset, scale, opacity, z }) => {
|
||||
const isCenter = offset === 0;
|
||||
const slide = sliderSlides[getIndex(offset)];
|
||||
if (!isCenter && len < 2) return null;
|
||||
const slide = slides[getIndex(offset)];
|
||||
|
||||
const heightClass = `w-full h-[200px] sm:h-[260px] md:h-[310px] ${
|
||||
isCenter ? "lg:h-[380px]" : "lg:h-[350px]"
|
||||
}`;
|
||||
|
||||
const media = remote ? (
|
||||
<SlotImage
|
||||
url={slide.src}
|
||||
title={slide.title}
|
||||
linkUrl={isCenter ? slide.linkUrl : undefined}
|
||||
className="block h-full"
|
||||
imgClassName={`${heightClass} object-cover`}
|
||||
/>
|
||||
) : (
|
||||
<Placeholder
|
||||
src={slide.src}
|
||||
alt={slide.title}
|
||||
width="100%"
|
||||
label={isCenter ? "1280×380" : "880×350"}
|
||||
className={`rounded-none border-0 ${heightClass}`}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -38,15 +80,7 @@ export function HeroSlider() {
|
||||
} ${isCenter ? "" : "pointer-events-none"}`}
|
||||
>
|
||||
<div className="overflow-hidden rounded-2xl border border-border bg-surface shadow-lg">
|
||||
<Placeholder
|
||||
src={slide.image}
|
||||
alt={slide.title}
|
||||
width="100%"
|
||||
label={isCenter ? "1280×380" : "880×350"}
|
||||
className={`w-full rounded-none border-0 h-[200px] sm:h-[260px] md:h-[310px] ${
|
||||
isCenter ? "lg:h-[380px]" : "lg:h-[350px]"
|
||||
}`}
|
||||
/>
|
||||
{media}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -56,16 +90,14 @@ export function HeroSlider() {
|
||||
<div className="mt-5 flex items-center justify-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setActive((prev) => (prev - 1 + sliderSlides.length) % sliderSlides.length)
|
||||
}
|
||||
onClick={() => setActive((prev) => (prev - 1 + len) % len)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full border border-border text-red transition-colors hover:border-red hover:bg-red-light"
|
||||
aria-label="اسلاید قبلی"
|
||||
>
|
||||
→
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
{sliderSlides.map((slide, i) => (
|
||||
{slides.map((slide, i) => (
|
||||
<button
|
||||
key={slide.id}
|
||||
type="button"
|
||||
@@ -79,7 +111,7 @@ export function HeroSlider() {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActive((prev) => (prev + 1) % sliderSlides.length)}
|
||||
onClick={() => setActive((prev) => (prev + 1) % len)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full border border-border text-red transition-colors hover:border-red hover:bg-red-light"
|
||||
aria-label="اسلاید بعدی"
|
||||
>
|
||||
|
||||
@@ -2,14 +2,35 @@ import { Carousel } from "@/components/ui/Carousel";
|
||||
import { Placeholder } from "@/components/ui/Placeholder";
|
||||
import { ProductCard } from "@/components/ui/ProductCard";
|
||||
import { SectionTitle } from "@/components/ui/SectionTitle";
|
||||
import { SlotImage } from "@/components/ui/SlotImage";
|
||||
import { specialSaleProducts } from "@/data/sample";
|
||||
import type { StaticImageWithUrl } from "@/lib/meshkee";
|
||||
|
||||
type SpecialSaleCarouselProps = {
|
||||
banner?: StaticImageWithUrl;
|
||||
};
|
||||
|
||||
function SpecialSaleBanner({ banner }: { banner?: StaticImageWithUrl }) {
|
||||
const frameClassName =
|
||||
"relative w-[220px] shrink-0 self-stretch overflow-hidden rounded-xl border border-border md:w-[240px]";
|
||||
|
||||
if (banner) {
|
||||
return (
|
||||
<div className={frameClassName}>
|
||||
<SlotImage
|
||||
url={banner.url}
|
||||
title={banner.title}
|
||||
linkUrl={banner.linkUrl}
|
||||
alt={banner.title ?? "فروش ویژه"}
|
||||
className="block h-full"
|
||||
imgClassName="h-full min-h-[380px] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SpecialSaleBanner() {
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className="relative w-[220px] shrink-0 self-stretch overflow-hidden rounded-xl border border-border md:w-[240px]"
|
||||
>
|
||||
<a href="#" className={frameClassName}>
|
||||
<Placeholder
|
||||
src="/images/sale-banner.svg"
|
||||
alt="فروش ویژه"
|
||||
@@ -21,7 +42,7 @@ function SpecialSaleBanner() {
|
||||
);
|
||||
}
|
||||
|
||||
export function SpecialSaleCarousel() {
|
||||
export function SpecialSaleCarousel({ banner }: SpecialSaleCarouselProps) {
|
||||
return (
|
||||
<section className="border-y border-red-light bg-red-light/30 py-10 md:py-14">
|
||||
<div className="mx-auto max-w-7xl px-4">
|
||||
@@ -31,7 +52,7 @@ export function SpecialSaleCarousel() {
|
||||
action="مشاهده همه"
|
||||
/>
|
||||
<Carousel ariaLabel="کاروسل فروش ویژه">
|
||||
<SpecialSaleBanner />
|
||||
<SpecialSaleBanner banner={banner} />
|
||||
{specialSaleProducts.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
|
||||
@@ -1,50 +1,71 @@
|
||||
import { Placeholder } from "@/components/ui/Placeholder";
|
||||
import { SlotImage } from "@/components/ui/SlotImage";
|
||||
import type { StaticImageWithUrl } from "@/lib/meshkee";
|
||||
|
||||
export function TwoBanners() {
|
||||
type TwoBannersProps = {
|
||||
images?: StaticImageWithUrl[];
|
||||
};
|
||||
|
||||
const fallbackBanners = [
|
||||
{
|
||||
src: "/images/banner-1.svg",
|
||||
alt: "تخفیف ویژه یخچال و فریزر",
|
||||
kicker: "Summer Sale",
|
||||
title: "تخفیف ویژه یخچال و فریزر",
|
||||
subtitle: "تا ۲۵٪ تخفیف — تا پایان مرداد",
|
||||
},
|
||||
{
|
||||
src: "/images/banner-2.svg",
|
||||
alt: "تلویزیونهای هوشمند ۲۰۲۶",
|
||||
kicker: "Smart TV",
|
||||
title: "تلویزیونهای هوشمند ۲۰۲۶",
|
||||
subtitle: "QLED و OLED با اقساط ۱۲ ماهه",
|
||||
},
|
||||
];
|
||||
|
||||
export function TwoBanners({ images = [] }: TwoBannersProps) {
|
||||
return (
|
||||
<section className="py-10 md:py-14">
|
||||
<div className="mx-auto max-w-7xl px-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<a
|
||||
href="#"
|
||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||
>
|
||||
<Placeholder
|
||||
src="/images/banner-1.svg"
|
||||
alt="تخفیف ویژه یخچال و فریزر"
|
||||
width="100%"
|
||||
height="200px"
|
||||
label="640×200"
|
||||
className="w-full rounded-none border-0"
|
||||
/>
|
||||
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-red-dark/80 to-transparent p-6">
|
||||
<p className="font-display text-xs tracking-widest text-red-muted uppercase">
|
||||
Summer Sale
|
||||
</p>
|
||||
<h3 className="text-lg font-bold text-white">تخفیف ویژه یخچال و فریزر</h3>
|
||||
<p className="mt-1 text-sm text-white/80">تا ۲۵٪ تخفیف — تا پایان مرداد</p>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||
>
|
||||
<Placeholder
|
||||
src="/images/banner-2.svg"
|
||||
alt="تلویزیونهای هوشمند ۲۰۲۶"
|
||||
width="100%"
|
||||
height="200px"
|
||||
label="640×200"
|
||||
className="w-full rounded-none border-0"
|
||||
/>
|
||||
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-red-dark/80 to-transparent p-6">
|
||||
<p className="font-display text-xs tracking-widest text-red-muted uppercase">
|
||||
Smart TV
|
||||
</p>
|
||||
<h3 className="text-lg font-bold text-white">تلویزیونهای هوشمند ۲۰۲۶</h3>
|
||||
<p className="mt-1 text-sm text-white/80">QLED و OLED با اقساط ۱۲ ماهه</p>
|
||||
</div>
|
||||
</a>
|
||||
{images.length > 0
|
||||
? images.map((image) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||
>
|
||||
<SlotImage
|
||||
url={image.url}
|
||||
title={image.title}
|
||||
linkUrl={image.linkUrl}
|
||||
className="block"
|
||||
imgClassName="block h-[200px] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: fallbackBanners.map((banner) => (
|
||||
<a
|
||||
key={banner.src}
|
||||
href="#"
|
||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||
>
|
||||
<Placeholder
|
||||
src={banner.src}
|
||||
alt={banner.alt}
|
||||
width="100%"
|
||||
height="200px"
|
||||
label="640×200"
|
||||
className="w-full rounded-none border-0"
|
||||
/>
|
||||
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-red-dark/80 to-transparent p-6">
|
||||
<p className="font-display text-xs tracking-widest text-red-muted uppercase">
|
||||
{banner.kicker}
|
||||
</p>
|
||||
<h3 className="text-lg font-bold text-white">{banner.title}</h3>
|
||||
<p className="mt-1 text-sm text-white/80">{banner.subtitle}</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
type SlotImageProps = {
|
||||
url: string;
|
||||
title?: string | null;
|
||||
linkUrl?: string | null;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
imgClassName?: string;
|
||||
};
|
||||
|
||||
export function SlotImage({
|
||||
url,
|
||||
title,
|
||||
linkUrl,
|
||||
alt,
|
||||
className,
|
||||
imgClassName = "block h-full w-full object-cover",
|
||||
}: SlotImageProps) {
|
||||
const img = (
|
||||
// Meshkee slot URLs are used as-is; do not route through next/image.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={url} alt={alt ?? title ?? ""} className={imgClassName} />
|
||||
);
|
||||
|
||||
if (linkUrl) {
|
||||
return (
|
||||
<a href={linkUrl} className={className}>
|
||||
{img}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (className) {
|
||||
return <div className={className}>{img}</div>;
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
Reference in New Issue
Block a user