204 lines
8.4 KiB
TypeScript
204 lines
8.4 KiB
TypeScript
"use client"
|
|
|
|
import Image from "next/image"
|
|
import { useEffect, useState } from "react"
|
|
import { motion } from "framer-motion"
|
|
import { ensureHttpsImageUrl } from "@/lib/utils"
|
|
|
|
interface Logo {
|
|
id: string
|
|
src: string
|
|
alt: string
|
|
}
|
|
|
|
|
|
export default function TrustedBy() {
|
|
const [heading, setHeading] = useState("Trusted by leading schools worldwide")
|
|
const [subtext, setSubtext] = useState(
|
|
"Join the growing community of schools that trust our platform to transform education through technology."
|
|
)
|
|
const [logos, setLogos] = useState<Logo[]>([])
|
|
|
|
useEffect(() => {
|
|
const loadTexts = async () => {
|
|
try {
|
|
// Use Next.js API route instead of direct Strapi calls
|
|
const res = await fetch("/api/trusted-by", {
|
|
headers: { Accept: "application/json" },
|
|
})
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`Trusted-by API status ${res.status}`)
|
|
}
|
|
|
|
const data = await res.json()
|
|
|
|
// Extract heading
|
|
if (data.heading) {
|
|
const headingJson = data.heading
|
|
const text =
|
|
headingJson?.data?.attributes?.text ??
|
|
headingJson?.data?.text ??
|
|
headingJson?.data?.TEXT ??
|
|
headingJson?.data?.attributes?.TEXT
|
|
if (text) setHeading(text)
|
|
}
|
|
|
|
// Extract subtext
|
|
if (data.subtext) {
|
|
const subtextJson = data.subtext
|
|
const text =
|
|
subtextJson?.data?.attributes?.text ??
|
|
subtextJson?.data?.text ??
|
|
subtextJson?.data?.TEXT ??
|
|
subtextJson?.data?.attributes?.TEXT
|
|
if (text) setSubtext(text)
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to load trusted-by texts:", err)
|
|
}
|
|
}
|
|
|
|
loadTexts()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const loadLogos = async () => {
|
|
try {
|
|
// Use Next.js API route instead of direct Strapi call
|
|
const res = await fetch("/api/trusted-by", {
|
|
headers: { Accept: "application/json" },
|
|
})
|
|
if (!res.ok) throw new Error(`Trusted-by API status ${res.status}`)
|
|
const data = await res.json()
|
|
|
|
if (!data.logos) return
|
|
|
|
const json = data.logos
|
|
const entries = Array.isArray(json?.data) ? json.data : []
|
|
|
|
// Collect all media URLs from trusted_schools relation
|
|
const collected: Logo[] = []
|
|
entries.forEach((item: any, idx: number) => {
|
|
const attrs = item?.attributes ?? item ?? {}
|
|
const media =
|
|
attrs?.trusted_schools?.data ||
|
|
attrs?.trusted_schools || // handle array directly (as seen in response)
|
|
attrs?.trusted_school?.data ||
|
|
attrs?.trustedSchools?.data
|
|
const mediaArray = Array.isArray(media) ? media : Array.isArray(media?.data) ? media.data : []
|
|
|
|
mediaArray.forEach((m: any, mIdx: number) => {
|
|
const a = m?.attributes ?? m ?? {}
|
|
const url =
|
|
a?.url ||
|
|
a?.formats?.medium?.url ||
|
|
a?.formats?.small?.url ||
|
|
a?.formats?.thumbnail?.url ||
|
|
""
|
|
if (!url) return
|
|
// Ensure HTTPS to prevent Mixed Content errors
|
|
const absolute = ensureHttpsImageUrl(url)
|
|
collected.push({
|
|
id: `${item?.id ?? idx}-${m?.id ?? mIdx}`,
|
|
src: absolute,
|
|
alt: a?.alternativeText || "Trusted school",
|
|
})
|
|
})
|
|
})
|
|
|
|
if (collected.length > 0) {
|
|
setLogos(collected)
|
|
}
|
|
// Don't set fallback - user wants only Strapi images
|
|
} catch (err) {
|
|
console.error("Failed to load trusted schools logos:", err)
|
|
// Don't set fallback - user wants only Strapi images
|
|
}
|
|
}
|
|
|
|
loadLogos()
|
|
}, [])
|
|
|
|
const twoImages = [
|
|
{
|
|
id: "4",
|
|
alt: "Headshot of a smiling young man with short brown hair and blue eyes.",
|
|
src: "http://160.187.167.213/uploads/image_7e177c57da.png"
|
|
},
|
|
];
|
|
|
|
const renderedLogos = logos.length > 0 ? logos : twoImages;
|
|
|
|
return (
|
|
<section className="w-full bg-white py-16">
|
|
<div className="max-w-[1400px] mx-auto px-4 mb-12 text-center">
|
|
<h2 className="text-3xl md:text-4xl font-bold text-[#353535] mb-4">
|
|
{heading}
|
|
</h2>
|
|
<p className="text-gray-600 text-sm md:text-base max-w-2xl mx-auto">
|
|
{subtext}
|
|
</p>
|
|
</div>
|
|
|
|
{renderedLogos.length > 0 && (
|
|
<div className="relative max-w-6xl mx-auto px-4">
|
|
<div className="relative rounded-3xl border border-gray-300 px-4 md:px-8 py-6 md:py-8 shadow-sm bg-white overflow-hidden">
|
|
{renderedLogos.length > 2 ? (
|
|
<motion.div
|
|
className="flex items-center gap-8 md:gap-10"
|
|
animate={{ x: ["0%", "-50%"] }}
|
|
transition={{
|
|
x: {
|
|
repeat: Infinity,
|
|
repeatType: "loop",
|
|
duration: 18,
|
|
ease: "linear",
|
|
},
|
|
}}
|
|
style={{ width: "max-content" }}
|
|
>
|
|
{renderedLogos.concat(renderedLogos).map((item, idx) => (
|
|
<div
|
|
key={`${item.id}-${idx}`}
|
|
className="min-w-[220px] md:min-w-[240px] flex items-center justify-center"
|
|
style={{ minHeight: "120px" }}
|
|
>
|
|
<Image
|
|
src={item.src}
|
|
alt={item.alt}
|
|
width={180}
|
|
height={140}
|
|
className="object-contain h-[120px] md:h-[140px]"
|
|
style={{ width: "auto", maxWidth: "180px" }}
|
|
/>
|
|
</div>
|
|
))}
|
|
</motion.div>
|
|
) : (
|
|
<div className="flex items-center justify-center gap-8 md:gap-12 py-4">
|
|
{renderedLogos.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="flex items-center justify-center px-4"
|
|
style={{ minHeight: "120px" }}
|
|
>
|
|
<Image
|
|
src={item.src}
|
|
alt={item.alt}
|
|
width={180}
|
|
height={140}
|
|
className="object-contain h-[120px] md:h-[140px]"
|
|
style={{ width: "auto", maxWidth: "180px" }}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|