138 lines
6.1 KiB
TypeScript
138 lines
6.1 KiB
TypeScript
import { FlaskConical, Smartphone } from "lucide-react"
|
|
import Link from "next/link"
|
|
|
|
type Offering = {
|
|
id: number | string
|
|
title: string
|
|
description: string
|
|
info_footer: string
|
|
svg?: string | null
|
|
// Fallback icon key (not used when svg exists)
|
|
icon?: typeof FlaskConical
|
|
}
|
|
|
|
async function fetchOfferings(): Promise<Offering[]> {
|
|
try {
|
|
// For server components, we can fetch directly from Strapi since there's no CORS issue
|
|
// But for consistency, we'll use the API route
|
|
const baseUrl = process.env.NEXT_PUBLIC_STRAPI_BASE_URL || "http://160.187.167.213"
|
|
const apiUrl = `${baseUrl.replace(/\/+$/, "")}/api/offerings`
|
|
|
|
const res = await fetch(apiUrl, {
|
|
headers: { Accept: "application/json" },
|
|
next: { revalidate: 300 },
|
|
})
|
|
|
|
if (!res.ok) throw new Error(`Offerings API status ${res.status}`)
|
|
|
|
const json = await res.json()
|
|
const items = Array.isArray(json?.data) ? [...json.data].reverse() : []
|
|
|
|
return items.map((item: any, index: number) => {
|
|
// Strapi v4: attributes, but sample payload has fields at root; handle both
|
|
const attrs = item?.attributes ?? item ?? {}
|
|
|
|
// Optional fallback icon rotation if SVG missing
|
|
const fallbackIcons = [FlaskConical, Smartphone]
|
|
|
|
return {
|
|
id: item?.id ?? attrs?.id ?? index,
|
|
title: attrs?.title ?? "",
|
|
description: attrs?.description ?? "",
|
|
info_footer: attrs?.info_footer ?? "",
|
|
svg: attrs?.svg_icon ?? null,
|
|
icon: fallbackIcons[index % fallbackIcons.length],
|
|
} satisfies Offering
|
|
})
|
|
} catch (error) {
|
|
console.error("Failed to load offerings:", error)
|
|
// Fallback to empty array if API fails
|
|
return []
|
|
}
|
|
}
|
|
|
|
async function fetchOfferingHeading() {
|
|
try {
|
|
// For server components, we can fetch directly from Strapi since there's no CORS issue
|
|
const baseUrl = process.env.NEXT_PUBLIC_STRAPI_BASE_URL || "http://160.187.167.213"
|
|
const apiUrl = `${baseUrl.replace(/\/+$/, "")}/api/offering-text`
|
|
|
|
const res = await fetch(apiUrl, {
|
|
headers: { Accept: "application/json" },
|
|
next: { revalidate: 300 },
|
|
})
|
|
|
|
if (!res.ok) throw new Error(`Offering heading API status ${res.status}`)
|
|
const json = await res.json()
|
|
|
|
// Handle both Strapi shapes (with or without attributes wrapper)
|
|
const attrs = json?.data?.attributes ?? json?.data ?? {}
|
|
return attrs?.offering || "Our Offerings"
|
|
} catch (error) {
|
|
console.error("Failed to load offering heading:", error)
|
|
return "Our Offerings"
|
|
}
|
|
}
|
|
|
|
export default async function OurOfferings() {
|
|
const heading = await fetchOfferingHeading()
|
|
const offerings = await fetchOfferings()
|
|
|
|
return (
|
|
<section className="w-full bg-white py-16 md:py-24 px-4">
|
|
<div className="max-w-6xl mx-auto">
|
|
<h2 className="text-4xl md:text-5xl font-bold text-[#353535] text-center mb-16">
|
|
{heading}
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
|
{offerings.map((offering) => (
|
|
<div
|
|
key={offering.id}
|
|
className="group relative bg-[#EAEAEA] hover:bg-[#353535] transition-colors duration-300 overflow-hidden md:aspect-square flex flex-col justify-center p-6 md:p-12"
|
|
>
|
|
{/* Icon */}
|
|
<div className="mb-6 w-12 h-12 md:w-16 md:h-16 flex items-center justify-start">
|
|
{offering.svg ? (
|
|
<span
|
|
aria-hidden
|
|
dangerouslySetInnerHTML={{
|
|
__html: offering.svg
|
|
.replace(/width="[^"]*"/, '')
|
|
.replace(/height="[^"]*"/, '')
|
|
.replace('<svg', '<svg preserveAspectRatio="xMidYMid meet" style="width: 100%; height: auto;"')
|
|
}}
|
|
className="block w-10 h-10 md:w-14 md:h-14 text-[#353535] group-hover:text-[#F48120] transition-colors duration-300 [&>svg]:w-full [&>svg]:h-auto [&>svg]:fill-none [&>svg]:stroke-current [&>svg]:shrink-0 [&>svg]:[stroke-width:1]"
|
|
/>
|
|
) : (
|
|
offering.icon && (
|
|
<offering.icon
|
|
strokeWidth={1}
|
|
className="w-10 h-10 md:w-14 md:h-14 text-[#353535] group-hover:text-[#F48120] transition-colors duration-300 shrink-0"
|
|
/>
|
|
)
|
|
)}
|
|
</div>
|
|
|
|
{/* Title */}
|
|
<h3 className="text-3xl font-bold text-[#353535] group-hover:text-white mb-4 transition-colors duration-300">
|
|
{offering.title}
|
|
</h3>
|
|
|
|
{/* Description */}
|
|
<p className="text-base text-[#353535] group-hover:text-white/90 mb-8 leading-relaxed transition-colors duration-300 max-w-sm whitespace-pre-line">
|
|
{offering.description}
|
|
</p>
|
|
|
|
{/* Info footer / link text */}
|
|
<span className="inline-block text-xs font-bold uppercase tracking-wider text-[#353535] group-hover:text-[#F48120] transition-colors duration-300">
|
|
{offering.info_footer}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|