This commit is contained in:
Vula Builder
2026-10-11 14:20:27 +00:00
parent e58dd7e210
commit 5261fef2a8
+110
View File
@@ -0,0 +1,110 @@
import HeroSlideshow, { type HeroSlide } from "@/components/hero/HeroSlideshow"
import { getAdvertBanners } from "@/lib/cms"
// The slides this site always shows. Banners published in Content Studio
// are shown first, while they are within their start and end dates.
const SLIDES: HeroSlide[] = [
{
primary: true,
image: "https://images.pexels.com/photos/4083586/pexels-photo-4083586.jpeg?auto=compress&cs=tinysrgb&h=650&w=940",
alt: "Ben's Noodle House",
eyebrow: "Ben's Noodle House",
headline: "Madadeni's first dedicated Chinese noodle House.",
subtitle: "Where authentic wok technique meets a South African kick, served piping hot in classic takeaway boxes right in the heart of the township.",
ctaText: "View our menu",
ctaHref: "/menu",
},
]
const DAY_MS = 24 * 60 * 60 * 1000
async function bannerSlides(): Promise<HeroSlide[]> {
const now = Date.now()
const items = await getAdvertBanners()
return items.flatMap((item) => {
const d = item.data as {
headline?: string; subheadline?: string; cta_text?: string; cta_url?: string
image?: string; start_date?: string; end_date?: string
}
const headline = (d.headline ?? "").trim()
const image = (d.image ?? "").trim()
if (!headline && !image) return []
const start = d.start_date ? new Date(d.start_date).getTime() : -Infinity
// A plain date ends at the END of that day, not the first second of it.
const end = d.end_date ? new Date(d.end_date).getTime() + (d.end_date.length === 10 ? DAY_MS : 0) : Infinity
if (!(now >= start && now < end)) return []
const ownButton = Boolean(d.cta_text && d.cta_url)
if (!headline) {
// A photo with no words is a poster or flyer: its message is in the
// picture, so it is shown whole, with a button only if it has its own.
return [{
image,
alt: item.title || "Banner",
eyebrow: "",
headline: "",
subtitle: "",
ctaText: ownButton ? d.cta_text! : "",
ctaHref: ownButton ? d.cta_url! : "",
}]
}
return [{
image: image || SLIDES[0].image,
alt: headline,
eyebrow: "",
headline,
subtitle: d.subheadline ?? "",
ctaText: ownButton ? d.cta_text! : SLIDES[0].ctaText,
ctaHref: ownButton ? d.cta_url! : SLIDES[0].ctaHref,
}]
})
}
const STORE_URL = process.env.VULA_CMS_URL || "https://ide.vulai.co.za"
const STORE_ID = process.env.VULA_CMS_PROJECT_ID || ""
const CURRENCY: Record<string, string> = { zar: "R", usd: "$", eur: "€", gbp: "£" }
interface StoreItem {
id: string
title: string
thumbnail: string | null
variants?: Array<{ prices?: Array<{ amount: number; currency_code: string }> }>
}
function priceOf(item: StoreItem): string {
const price = item.variants?.[0]?.prices?.[0]
if (!price) return ""
const amount = price.amount / 100
const symbol = CURRENCY[price.currency_code?.toLowerCase()] ?? ""
return symbol + (Number.isInteger(amount) ? String(amount) : amount.toFixed(2))
}
// Two things this business actually sells, with their photos — read from its
// own catalogue, so the slides change when the catalogue does.
async function catalogueSlides(): Promise<HeroSlide[]> {
if (!STORE_ID) return []
try {
const res = await fetch(STORE_URL + "/api/store/" + STORE_ID + "/products?limit=12", { next: { revalidate: 300 } })
if (!res.ok) return []
const data = (await res.json()) as { products?: StoreItem[] }
return (data.products ?? [])
.filter((p) => p.title && typeof p.thumbnail === "string" && p.thumbnail.startsWith("https://"))
.slice(0, 2)
.map((p) => ({
image: p.thumbnail as string,
alt: p.title,
eyebrow: "Ben's Noodle House",
headline: p.title,
subtitle: priceOf(p),
ctaText: SLIDES[0].ctaText,
ctaHref: SLIDES[0].ctaHref,
}))
} catch {
return []
}
}
export default async function HeroSection() {
const [banners, catalogue] = await Promise.all([bannerSlides(), catalogueSlides()])
const slides = [...banners, ...SLIDES, ...catalogue].slice(0, 6)
return <HeroSlideshow slides={slides} />
}