Merge remote-tracking branch into main, resolving conflicts

Kept the local branch's homepage implementation (Meshkee product/category
API integration, redesigned header, Swiper carousels, hardcoded local
slider/banner images) as the source of truth for all conflicting sections,
since it reflects the most recent explicit direction.

Folded in the remote's non-conflicting additions: the static-image-slot
types/helpers in lib/meshkee.ts, the SlotImage component, and the
/meshkee/static-image-slots catalog route. These aren't wired into any
page yet since the live API's slot key ("home-hero") doesn't match the
keys this code expects ("slider", etc.) — worth reconciling once that's
settled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
amirhosein.ashourloo
2026-08-17 10:45:54 +03:30
co-authored by Claude Sonnet 5
4 changed files with 143 additions and 1 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
allowedDevOrigins: ["sanihome.com", "localhost"],
};
export default nextConfig;
@@ -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",
},
},
);
}
+37
View File
@@ -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
<img src={url} alt={alt ?? title ?? ""} className={imgClassName} />
);
if (linkUrl) {
return (
<a href={linkUrl} className={className}>
{img}
</a>
);
}
if (className) {
return <div className={className}>{img}</div>;
}
return img;
}
+91
View File
@@ -200,3 +200,94 @@ export async function getSliderSlides(): Promise<Slide[]> {
| undefined;
return (slides ?? []).map(normalizeSlide);
}
// 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.
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,
},
];
export async function getStaticImageSlots(): Promise<StaticImageSlot[]> {
const res = await apiGet<{ items: StaticImageSlot[] }>(`/tenants/${DOMAIN}/website/static-images`);
return res?.items ?? [];
}
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];
}