Deploy
This commit is contained in:
@@ -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} />
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user