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
+34 -12
View File
@@ -18,33 +18,55 @@ import {
getProducts,
getBrandGroups,
getStoreSpecials,
getStaticImageSlots,
imagesForSlot,
STATIC_IMAGE_KEYS,
} from "@/lib/meshkee";
export default async function Home() {
const [categoryGroups, categories, storeSpecials, productsResult, brandGroups, businessInfo] =
await Promise.all([
getCategoryGroups(),
getCategories(),
getStoreSpecials(),
getProducts({ pageSize: 12 }),
getBrandGroups(),
getBusinessInfo(),
]);
const [
categoryGroups,
categories,
storeSpecials,
productsResult,
brandGroups,
businessInfo,
staticImageSlots,
] = await Promise.all([
getCategoryGroups(),
getCategories(),
getStoreSpecials(),
getProducts({ pageSize: 12 }),
getBrandGroups(),
getBusinessInfo(),
getStaticImageSlots(),
]);
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 (
<>
<TopBar />
<Header categoryGroups={categoryGroups} categories={categories} />
<main>
<HeroSlider />
<HeroSlider images={heroImages} />
<SpecialSaleCarousel products={specialSaleProducts} />
<FeatureBar />
<FeaturedCategories categories={categories} />
<TwoBanners />
<TwoBanners
images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.twoBanner)}
aspectRatio={twoBannerSlot?.aspectRatio}
/>
<TopSellingCarousel products={productsResult.items} />
<ThreeBanners />
<ThreeBanners
images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.threeBannerRow)}
aspectRatio={threeBannerSlot?.aspectRatio}
/>
<BrandGroupCarousels brandGroups={brandGroups} />
<AboutSanihome />
<BlogCarousel />
+39 -14
View File
@@ -3,12 +3,29 @@
import { useState } from "react";
import { Placeholder } from "@/components/ui/Placeholder";
import { sliderSlides } from "@/data/sample";
import type { StaticImageWithUrl } from "@/lib/meshkee";
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 len = sliderSlides.length;
const len = slides.length;
const getDiff = (i: number) => {
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">
<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]">
{sliderSlides.map((slide, i) => {
{slides.map((slide, i) => {
const diff = getDiff(i);
const isCenter = diff === 0;
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 (
<div
key={slide.id}
@@ -41,12 +67,13 @@ export function HeroSlider() {
} ${isVisible ? "" : "opacity-0"}`}
style={{ transform: `translateX(${-diff * PEEK_OFFSET}%)` }}
>
<Placeholder
src={slide.image}
alt={slide.title}
width="100%"
className="h-full w-full rounded-none border-0"
/>
{slide.href ? (
<a href={slide.href} className="block h-full w-full">
{media}
</a>
) : (
media
)}
</div>
</div>
);
@@ -56,16 +83,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 +104,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="اسلاید بعدی"
>
+45 -15
View File
@@ -1,24 +1,54 @@
const banners = [
{ alt: "خانه‌ای تمیزتر، نفس‌هایی آسوده‌تر", image: "/images/banner3/6.jpg" },
{ alt: "خنکای دلنشین، در تمام لحظه‌ها", image: "/images/banner3/7.jpg" },
{ alt: "هر تصویر، یک تجربه تازه", image: "/images/banner3/8.jpg" },
import type { StaticImageWithUrl } from "@/lib/meshkee";
const fallbackBanners = [
{ 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 (
<section className="border-y border-border bg-background py-10 md:py-14">
<div className="mx-auto max-w-7xl px-4">
<div className="grid gap-4 md:grid-cols-3">
{banners.map((banner) => (
<a
key={banner.image}
href="#"
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 */}
<img src={banner.image} alt={banner.alt} className="h-auto w-full" />
</a>
))}
{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
key={banner.src}
href="#"
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 */}
<img src={banner.src} alt={banner.alt} className="h-auto w-full" />
</a>
))}
</div>
</div>
</section>
+46 -19
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 (
<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 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="/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
src="/images/banner/4.jpg"
alt="قدرت بخار، درخشش بی‌نقص"
className="h-auto w-full"
/>
</a>
{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-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>
))}
</div>
</div>
</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
// fixed homepage slots. Not currently wired into any page — the slot keys
// the live API returns (e.g. "home-hero") don't match STATIC_IMAGE_KEYS yet.
// fixed homepage slots, managed by the client through the Meshkee admin panel.
export type StaticImage = {
id: string;
url?: string | null;
title?: string | null;
titleFa?: string | null;
titleEn?: string | null;
subtext?: string | null;
linkUrl?: string | null;
width?: number | null;
height?: number | null;
@@ -219,62 +220,56 @@ export type StaticImageWithUrl = StaticImage & { url: string };
export type StaticImageSlot = {
id: string;
key: string;
pageKey?: string;
kind: "single" | "list";
label?: string;
aspectRatio?: string;
recommendedWidth?: number;
itemCount?: number | null;
sortOrder?: number;
images?: StaticImage[];
};
export const STATIC_IMAGE_KEYS = {
slider: "slider",
bannerInSpecialSale: "banner-in-special-sale",
categories: "categories",
twoBannerRow: "two-banner-row",
homeHero: "home-hero",
twoBanner: "two-banner",
threeBannerRow: "three-banner-row",
} as const;
export const STATIC_IMAGE_SLOT_CATALOG = [
{
key: STATIC_IMAGE_KEYS.slider,
label: "Homepage slider",
key: STATIC_IMAGE_KEYS.homeHero,
label: "Homepage hero slider",
kind: "list" as const,
pageKey: "home",
aspectRatio: "16:9",
aspectRatio: "21:9",
itemCount: null,
recommendedWidth: 1440,
recommendedWidth: 1920,
},
{
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,
key: STATIC_IMAGE_KEYS.twoBanner,
label: "Two banner row",
kind: "list" as const,
pageKey: "home",
aspectRatio: "16:5",
aspectRatio: "12:5",
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[]> {
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 ?? [];
}