From 5261fef2a8d3174404ffda7d84b40fb4d7999dbe Mon Sep 17 00:00:00 2001 From: Vula Builder Date: Sun, 11 Oct 2026 14:20:27 +0000 Subject: [PATCH] Deploy --- src/components/sections/HeroSection.tsx | 110 ++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/components/sections/HeroSection.tsx diff --git a/src/components/sections/HeroSection.tsx b/src/components/sections/HeroSection.tsx new file mode 100644 index 0000000..e690346 --- /dev/null +++ b/src/components/sections/HeroSection.tsx @@ -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 { + 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 = { 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 { + 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 +}