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:
+1
-1
@@ -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",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user