diff --git a/next.config.ts b/next.config.ts index e9ffa30..ef8450f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + allowedDevOrigins: ["sanihome.com", "localhost"], }; export default nextConfig; diff --git a/src/app/meshkee/static-image-slots/route.ts b/src/app/meshkee/static-image-slots/route.ts new file mode 100644 index 0000000..49e9367 --- /dev/null +++ b/src/app/meshkee/static-image-slots/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; +import { STATIC_IMAGE_SLOT_CATALOG } from "@/lib/meshkee"; + +export function GET() { + return NextResponse.json( + { slots: STATIC_IMAGE_SLOT_CATALOG }, + { + headers: { + "Access-Control-Allow-Origin": "*", + "Cache-Control": "public, max-age=60", + }, + }, + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 3fef222..739c819 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -11,18 +11,30 @@ import { SamsungCarousel } from "@/components/home/SamsungCarousel"; import { AboutSanihome } from "@/components/home/AboutSanihome"; import { BlogCarousel } from "@/components/home/BlogCarousel"; import { Footer } from "@/components/home/Footer"; +import { + STATIC_IMAGE_KEYS, + firstImageForSlot, + getStaticImageSlots, + imagesForSlot, +} from "@/lib/meshkee"; + +export default async function Home() { + const slots = await getStaticImageSlots(); -export default function Home() { return ( <> - - + + - - + + diff --git a/src/components/home/FeaturedCategories.tsx b/src/components/home/FeaturedCategories.tsx index 4cebead..7a1fae4 100644 --- a/src/components/home/FeaturedCategories.tsx +++ b/src/components/home/FeaturedCategories.tsx @@ -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 ( @@ -12,27 +18,47 @@ export function FeaturedCategories() { action="همه دستهها" /> - {categories.map((cat, i) => ( - - - - {cat.name} - - {cat.count} محصول - - ))} + {images.length > 0 + ? images.map((image, i) => ( + + + {image.title ? ( + + {image.title} + + ) : null} + + )) + : categories.map((cat, i) => ( + + + + {cat.name} + + {cat.count} محصول + + ))} diff --git a/src/components/home/HeroSlider.tsx b/src/components/home/HeroSlider.tsx index 3622f5d..3eda2b0 100644 --- a/src/components/home/HeroSlider.tsx +++ b/src/components/home/HeroSlider.tsx @@ -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() { {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 ? ( + + ) : ( + + ); return ( - + {media} ); @@ -56,16 +90,14 @@ export function HeroSlider() { - 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="اسلاید قبلی" > → - {sliderSlides.map((slide, i) => ( + {slides.map((slide, i) => ( 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="اسلاید بعدی" > diff --git a/src/components/home/SpecialSaleCarousel.tsx b/src/components/home/SpecialSaleCarousel.tsx index 3e75764..7ab321e 100644 --- a/src/components/home/SpecialSaleCarousel.tsx +++ b/src/components/home/SpecialSaleCarousel.tsx @@ -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 ( + + + + ); + } -function SpecialSaleBanner() { return ( - + @@ -31,7 +52,7 @@ export function SpecialSaleCarousel() { action="مشاهده همه" /> - + {specialSaleProducts.map((product) => ( - - - - - Summer Sale - - تخفیف ویژه یخچال و فریزر - تا ۲۵٪ تخفیف — تا پایان مرداد - - - - - - - Smart TV - - تلویزیونهای هوشمند ۲۰۲۶ - QLED و OLED با اقساط ۱۲ ماهه - - + {images.length > 0 + ? images.map((image) => ( + + + + )) + : fallbackBanners.map((banner) => ( + + + + + {banner.kicker} + + {banner.title} + {banner.subtitle} + + + ))} diff --git a/src/components/ui/SlotImage.tsx b/src/components/ui/SlotImage.tsx new file mode 100644 index 0000000..49a3fdc --- /dev/null +++ b/src/components/ui/SlotImage.tsx @@ -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 + + ); + + if (linkUrl) { + return ( + + {img} + + ); + } + + if (className) { + return {img}; + } + + return img; +} diff --git a/src/lib/meshkee.ts b/src/lib/meshkee.ts new file mode 100644 index 0000000..cc2d9ab --- /dev/null +++ b/src/lib/meshkee.ts @@ -0,0 +1,104 @@ +export type StaticImage = { + id: string; + url?: string | null; + title?: string | null; + linkUrl?: string | null; + width?: number | null; + height?: number | null; + sortOrder?: number; +}; + +export type StaticImageWithUrl = StaticImage & { url: string }; + +export type StaticImageSlot = { + id: string; + key: string; + kind: "single" | "list"; + label?: string; + aspectRatio?: string; + recommendedWidth?: number; + sortOrder?: number; + images?: StaticImage[]; +}; + +export const STATIC_IMAGE_KEYS = { + slider: "slider", + bannerInSpecialSale: "banner-in-special-sale", + categories: "categories", + twoBannerRow: "two-banner-row", +} as const; + +export const STATIC_IMAGE_SLOT_CATALOG = [ + { + key: STATIC_IMAGE_KEYS.slider, + label: "Homepage slider", + kind: "list" as const, + pageKey: "home", + aspectRatio: "16:9", + itemCount: null, + recommendedWidth: 1440, + }, + { + key: STATIC_IMAGE_KEYS.bannerInSpecialSale, + label: "Banner in special sale", + kind: "single" as const, + pageKey: "home", + aspectRatio: "12:19", + itemCount: 1, + recommendedWidth: 480, + }, + { + key: STATIC_IMAGE_KEYS.categories, + label: "Featured category images", + kind: "list" as const, + pageKey: "home", + aspectRatio: "1:1", + itemCount: null, + recommendedWidth: 400, + }, + { + key: STATIC_IMAGE_KEYS.twoBannerRow, + label: "Two banner row", + kind: "list" as const, + pageKey: "home", + aspectRatio: "16:5", + itemCount: 2, + recommendedWidth: 1440, + }, +]; + +const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? ""; +const DOMAIN = process.env.NEXT_PUBLIC_DOMAIN ?? ""; + +export async function getStaticImageSlots(): Promise { + if (!API_BASE || !DOMAIN) return []; + + try { + const res = await fetch( + `${API_BASE}/tenants/${DOMAIN}/website/static-images`, + { cache: "no-store" }, + ); + if (!res.ok) return []; + const data: { items?: StaticImageSlot[] } = await res.json(); + return data.items ?? []; + } catch { + return []; + } +} + +export function imagesForSlot( + slots: StaticImageSlot[], + key: string, +): StaticImageWithUrl[] { + const images = slots.find((slot) => slot.key === key)?.images ?? []; + return [...images] + .filter((image): image is StaticImageWithUrl => Boolean(image.url)) + .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)); +} + +export function firstImageForSlot( + slots: StaticImageSlot[], + key: string, +): StaticImageWithUrl | undefined { + return imagesForSlot(slots, key)[0]; +}
- Summer Sale -
تا ۲۵٪ تخفیف — تا پایان مرداد
- Smart TV -
QLED و OLED با اقساط ۱۲ ماهه
+ {banner.kicker} +
{banner.subtitle}