set static images
This commit is contained in:
+27
-5
@@ -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 />
|
||||||
|
|||||||
@@ -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="اسلاید بعدی"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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 ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user