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