set static images

This commit is contained in:
amirhosein.ashourloo
2026-08-17 11:57:46 +03:30
parent a2eb413157
commit 1f81236d0a
5 changed files with 192 additions and 93 deletions
+27 -5
View File
@@ -18,33 +18,55 @@ import {
getProducts, getProducts,
getBrandGroups, getBrandGroups,
getStoreSpecials, getStoreSpecials,
getStaticImageSlots,
imagesForSlot,
STATIC_IMAGE_KEYS,
} from "@/lib/meshkee"; } from "@/lib/meshkee";
export default async function Home() { export default async function Home() {
const [categoryGroups, categories, storeSpecials, productsResult, brandGroups, businessInfo] = const [
await Promise.all([ categoryGroups,
categories,
storeSpecials,
productsResult,
brandGroups,
businessInfo,
staticImageSlots,
] = await Promise.all([
getCategoryGroups(), getCategoryGroups(),
getCategories(), getCategories(),
getStoreSpecials(), getStoreSpecials(),
getProducts({ pageSize: 12 }), getProducts({ pageSize: 12 }),
getBrandGroups(), getBrandGroups(),
getBusinessInfo(), getBusinessInfo(),
getStaticImageSlots(),
]); ]);
const specialSaleProducts = storeSpecials[0]?.items ?? []; const specialSaleProducts = storeSpecials[0]?.items ?? [];
const twoBannerSlot = staticImageSlots.find((slot) => slot.key === STATIC_IMAGE_KEYS.twoBanner);
const threeBannerSlot = staticImageSlots.find(
(slot) => slot.key === STATIC_IMAGE_KEYS.threeBannerRow,
);
const heroImages = imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.homeHero);
return ( return (
<> <>
<TopBar /> <TopBar />
<Header categoryGroups={categoryGroups} categories={categories} /> <Header categoryGroups={categoryGroups} categories={categories} />
<main> <main>
<HeroSlider /> <HeroSlider images={heroImages} />
<SpecialSaleCarousel products={specialSaleProducts} /> <SpecialSaleCarousel products={specialSaleProducts} />
<FeatureBar /> <FeatureBar />
<FeaturedCategories categories={categories} /> <FeaturedCategories categories={categories} />
<TwoBanners /> <TwoBanners
images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.twoBanner)}
aspectRatio={twoBannerSlot?.aspectRatio}
/>
<TopSellingCarousel products={productsResult.items} /> <TopSellingCarousel products={productsResult.items} />
<ThreeBanners /> <ThreeBanners
images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.threeBannerRow)}
aspectRatio={threeBannerSlot?.aspectRatio}
/>
<BrandGroupCarousels brandGroups={brandGroups} /> <BrandGroupCarousels brandGroups={brandGroups} />
<AboutSanihome /> <AboutSanihome />
<BlogCarousel /> <BlogCarousel />
+39 -14
View File
@@ -3,12 +3,29 @@
import { useState } from "react"; import { useState } from "react";
import { Placeholder } from "@/components/ui/Placeholder"; import { Placeholder } from "@/components/ui/Placeholder";
import { sliderSlides } from "@/data/sample"; import { sliderSlides } from "@/data/sample";
import type { StaticImageWithUrl } from "@/lib/meshkee";
const PEEK_OFFSET = 70; const PEEK_OFFSET = 70;
export function HeroSlider() { type Slide = { id: string; title: string; image: string; href?: string };
export function HeroSlider({ images = [] }: { images?: StaticImageWithUrl[] }) {
const slides: Slide[] =
images.length > 0
? images.map((image) => ({
id: image.id,
title: image.titleFa ?? image.titleEn ?? "",
image: image.url,
href: image.linkUrl ?? undefined,
}))
: sliderSlides.map((slide) => ({
id: String(slide.id),
title: slide.title,
image: slide.image,
}));
const [active, setActive] = useState(0); const [active, setActive] = useState(0);
const len = sliderSlides.length; const len = slides.length;
const getDiff = (i: number) => { const getDiff = (i: number) => {
let diff = i - active; let diff = i - active;
@@ -21,11 +38,20 @@ export function HeroSlider() {
<section className="overflow-x-clip bg-gradient-to-b from-red-light/30 to-background py-4 md:py-6"> <section className="overflow-x-clip bg-gradient-to-b from-red-light/30 to-background py-4 md:py-6">
<div className="relative mx-auto w-full max-w-7xl px-4"> <div className="relative mx-auto w-full max-w-7xl px-4">
<div className="relative h-[220px] sm:h-[280px] md:h-[340px] lg:h-[380px]"> <div className="relative h-[220px] sm:h-[280px] md:h-[340px] lg:h-[380px]">
{sliderSlides.map((slide, i) => { {slides.map((slide, i) => {
const diff = getDiff(i); const diff = getDiff(i);
const isCenter = diff === 0; const isCenter = diff === 0;
const isVisible = Math.abs(diff) <= 1; const isVisible = Math.abs(diff) <= 1;
const media = (
<Placeholder
src={slide.image}
alt={slide.title}
width="100%"
className="h-full w-full rounded-none border-0"
/>
);
return ( return (
<div <div
key={slide.id} key={slide.id}
@@ -41,12 +67,13 @@ export function HeroSlider() {
} ${isVisible ? "" : "opacity-0"}`} } ${isVisible ? "" : "opacity-0"}`}
style={{ transform: `translateX(${-diff * PEEK_OFFSET}%)` }} style={{ transform: `translateX(${-diff * PEEK_OFFSET}%)` }}
> >
<Placeholder {slide.href ? (
src={slide.image} <a href={slide.href} className="block h-full w-full">
alt={slide.title} {media}
width="100%" </a>
className="h-full w-full rounded-none border-0" ) : (
/> media
)}
</div> </div>
</div> </div>
); );
@@ -56,16 +83,14 @@ export function HeroSlider() {
<div className="mt-5 flex items-center justify-center gap-3"> <div className="mt-5 flex items-center justify-center gap-3">
<button <button
type="button" type="button"
onClick={() => onClick={() => setActive((prev) => (prev - 1 + len) % len)}
setActive((prev) => (prev - 1 + sliderSlides.length) % sliderSlides.length)
}
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" 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="اسلاید قبلی" aria-label="اسلاید قبلی"
> >
→ →
</button> </button>
<div className="flex gap-2"> <div className="flex gap-2">
{sliderSlides.map((slide, i) => ( {slides.map((slide, i) => (
<button <button
key={slide.id} key={slide.id}
type="button" type="button"
@@ -79,7 +104,7 @@ export function HeroSlider() {
</div> </div>
<button <button
type="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" 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="اسلاید بعدی" aria-label="اسلاید بعدی"
> >
+38 -8
View File
@@ -1,22 +1,52 @@
const banners = [ import type { StaticImageWithUrl } from "@/lib/meshkee";
{ alt: "خانه‌ای تمیزتر، نفس‌هایی آسوده‌تر", image: "/images/banner3/6.jpg" },
{ alt: "خنکای دلنشین، در تمام لحظه‌ها", image: "/images/banner3/7.jpg" }, const fallbackBanners = [
{ alt: "هر تصویر، یک تجربه تازه", image: "/images/banner3/8.jpg" }, { src: "/images/banner3/6.jpg", alt: "خانه‌ای تمیزتر، نفس‌هایی آسوده‌تر" },
{ src: "/images/banner3/7.jpg", alt: "خنکای دلنشین، در تمام لحظه‌ها" },
{ src: "/images/banner3/8.jpg", alt: "هر تصویر، یک تجربه تازه" },
]; ];
export function ThreeBanners() { export function ThreeBanners({
images = [],
aspectRatio,
}: {
images?: StaticImageWithUrl[];
aspectRatio?: string;
}) {
const style = aspectRatio ? { aspectRatio: aspectRatio.replace(":", " / ") } : undefined;
return ( return (
<section className="border-y border-border bg-background py-10 md:py-14"> <section className="border-y border-border bg-background py-10 md:py-14">
<div className="mx-auto max-w-7xl px-4"> <div className="mx-auto max-w-7xl px-4">
<div className="grid gap-4 md:grid-cols-3"> <div className="grid gap-4 md:grid-cols-3">
{banners.map((banner) => ( {images.length > 0
? images.map((image) => {
const frame = (
// eslint-disable-next-line @next/next/no-img-element
<img
src={image.url}
alt={image.titleFa ?? image.titleEn ?? ""}
style={style}
className="h-auto w-full object-cover"
/>
);
return (
<div
key={image.id}
className="group overflow-hidden rounded-xl border border-border bg-surface transition-all hover:border-red hover:shadow-md"
>
{image.linkUrl ? <a href={image.linkUrl}>{frame}</a> : frame}
</div>
);
})
: fallbackBanners.map((banner) => (
<a <a
key={banner.image} key={banner.src}
href="#" href="#"
className="group overflow-hidden rounded-xl border border-border bg-surface transition-all hover:border-red hover:shadow-md" className="group overflow-hidden rounded-xl border border-border bg-surface transition-all hover:border-red hover:shadow-md"
> >
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img src={banner.image} alt={banner.alt} className="h-auto w-full" /> <img src={banner.src} alt={banner.alt} className="h-auto w-full" />
</a> </a>
))} ))}
</div> </div>
+43 -16
View File
@@ -1,26 +1,53 @@
export function TwoBanners() { import type { StaticImageWithUrl } from "@/lib/meshkee";
const fallbackBanners = [
{ src: "/images/banner/5.jpg", alt: "وقتی که انرژی لازمی" },
{ src: "/images/banner/4.jpg", alt: "قدرت بخار، درخشش بی‌نقص" },
];
export function TwoBanners({
images = [],
aspectRatio,
}: {
images?: StaticImageWithUrl[];
aspectRatio?: string;
}) {
const style = aspectRatio ? { aspectRatio: aspectRatio.replace(":", " / ") } : undefined;
return ( return (
<section className="py-10 md:py-14"> <section className="py-10 md:py-14">
<div className="mx-auto max-w-7xl px-4"> <div className="mx-auto max-w-7xl px-4">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<a {images.length > 0
href="#" ? images.map((image) => {
className="group overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg" const frame = (
> // eslint-disable-next-line @next/next/no-img-element
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/images/banner/5.jpg" alt="وقتی که انرژی لازمی" className="h-auto w-full" />
</a>
<a
href="#"
className="group overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src="/images/banner/4.jpg" src={image.url}
alt="قدرت بخار، درخشش بی‌نقص" alt={image.titleFa ?? image.titleEn ?? ""}
className="h-auto w-full" style={style}
className="h-auto w-full object-cover"
/> />
);
return (
<div
key={image.id}
className="group overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
>
{image.linkUrl ? <a href={image.linkUrl}>{frame}</a> : frame}
</div>
);
})
: fallbackBanners.map((banner) => (
<a
key={banner.src}
href="#"
className="group overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={banner.src} alt={banner.alt} className="h-auto w-full" />
</a> </a>
))}
</div> </div>
</div> </div>
</section> </section>
+28 -33
View File
@@ -202,12 +202,13 @@ export async function getSliderSlides(): Promise<Slide[]> {
} }
// Admin-managed static/marketing images (slider photos, promo banners) for // Admin-managed static/marketing images (slider photos, promo banners) for
// fixed homepage slots. Not currently wired into any page — the slot keys // fixed homepage slots, managed by the client through the Meshkee admin panel.
// the live API returns (e.g. "home-hero") don't match STATIC_IMAGE_KEYS yet.
export type StaticImage = { export type StaticImage = {
id: string; id: string;
url?: string | null; url?: string | null;
title?: string | null; titleFa?: string | null;
titleEn?: string | null;
subtext?: string | null;
linkUrl?: string | null; linkUrl?: string | null;
width?: number | null; width?: number | null;
height?: number | null; height?: number | null;
@@ -219,62 +220,56 @@ export type StaticImageWithUrl = StaticImage & { url: string };
export type StaticImageSlot = { export type StaticImageSlot = {
id: string; id: string;
key: string; key: string;
pageKey?: string;
kind: "single" | "list"; kind: "single" | "list";
label?: string; label?: string;
aspectRatio?: string; aspectRatio?: string;
recommendedWidth?: number; recommendedWidth?: number;
itemCount?: number | null;
sortOrder?: number; sortOrder?: number;
images?: StaticImage[]; images?: StaticImage[];
}; };
export const STATIC_IMAGE_KEYS = { export const STATIC_IMAGE_KEYS = {
slider: "slider", homeHero: "home-hero",
bannerInSpecialSale: "banner-in-special-sale", twoBanner: "two-banner",
categories: "categories", threeBannerRow: "three-banner-row",
twoBannerRow: "two-banner-row",
} as const; } as const;
export const STATIC_IMAGE_SLOT_CATALOG = [ export const STATIC_IMAGE_SLOT_CATALOG = [
{ {
key: STATIC_IMAGE_KEYS.slider, key: STATIC_IMAGE_KEYS.homeHero,
label: "Homepage slider", label: "Homepage hero slider",
kind: "list" as const, kind: "list" as const,
pageKey: "home", pageKey: "home",
aspectRatio: "16:9", aspectRatio: "21:9",
itemCount: null, itemCount: null,
recommendedWidth: 1440, recommendedWidth: 1920,
}, },
{ {
key: STATIC_IMAGE_KEYS.bannerInSpecialSale, key: STATIC_IMAGE_KEYS.twoBanner,
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", label: "Two banner row",
kind: "list" as const, kind: "list" as const,
pageKey: "home", pageKey: "home",
aspectRatio: "16:5", aspectRatio: "12:5",
itemCount: 2, itemCount: 2,
recommendedWidth: 1440, recommendedWidth: 1200,
},
{
key: STATIC_IMAGE_KEYS.threeBannerRow,
label: "Three banner row",
kind: "list" as const,
pageKey: "home",
aspectRatio: "12:5",
itemCount: 3,
recommendedWidth: 1200,
}, },
]; ];
export async function getStaticImageSlots(): Promise<StaticImageSlot[]> { export async function getStaticImageSlots(): Promise<StaticImageSlot[]> {
const res = await apiGet<{ items: StaticImageSlot[] }>(`/tenants/${DOMAIN}/website/static-images`); const res = await apiGet<{ items: StaticImageSlot[] }>(`/tenants/${DOMAIN}/website/static-images`, {
pageKey: "home",
});
return res?.items ?? []; return res?.items ?? [];
} }