SFS/components/main-hero.tsx
2025-12-16 10:03:26 +05:30

325 lines
15 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
// --- Types & Data ---
interface ShapeData {
path: string
viewBox: string
// Optional scaling factor to visually normalize optical size if needed
scale?: number
// Custom styles if specific shapes need tweaks (e.g. fill)
fill?: boolean
strokeWidth?: number
}
const SHAPES: Record<string, ShapeData> = {
ladder: {
path: "M56.48 0V77.99H19.5601V0H0V354.08H19.5601V276.57H56.48V354.08H75.79V0H56.48ZM56.48 93.64V169.1H19.5601V93.64H56.48ZM56.48 260.93H19.5601V184.98H56.48V260.93Z",
viewBox: "0 0 76 355",
// Ladder is very tall/thin.
},
dna: {
path: "M147.696 201.09C151.706 184.52 146.746 166.92 132.936 148.8C121.456 133.73 105.836 123.73 89.9058 114.76C104.846 106.03 118.956 96.04 128.366 81.41C147.586 51.51 142.246 2.1 142.006 0.0100021L127.106 1.7C127.126 1.92 128.256 12.29 127.696 25.85H21.3659C20.7959 12.29 21.9159 1.90999 21.9359 1.68999L14.4858 0.849998L7.03583 0C6.79583 2.09 1.45585 51.5 20.6758 81.4C30.0858 96.03 44.1958 106.02 59.1358 114.75C43.2058 123.72 27.5858 133.73 16.1058 148.79C2.29584 166.91 -2.66417 184.5 1.34583 201.08C6.31583 221.58 24.9558 240.34 58.1558 258.19C1.86583 290.79 1.99581 336.03 2.07581 361.09C2.07581 364.17 2.09586 367.09 1.99586 369.52L16.9858 370.14C17.1058 367.37 17.0958 364.29 17.0858 361.04C17.0758 356.93 17.0558 352.5 17.2658 347.83H131.496V342.78C132.016 349.31 132.006 355.47 131.986 361.04C131.976 364.3 131.966 367.37 132.086 370.14L147.076 369.52C146.976 367.09 146.986 364.17 146.996 361.09C147.076 336.03 147.206 290.79 90.9158 258.19C124.126 240.34 142.766 221.58 147.726 201.08L147.696 201.09ZM126.636 38.85C125.726 46.2 124.196 53.81 121.706 60.85H27.3358C24.8558 53.82 23.3258 46.2 22.4158 38.85H126.636ZM33.6658 73.85H115.366C106.126 87.85 91.0058 97.11 74.5158 106.25C58.0258 97.1 42.9058 87.84 33.6658 73.85ZM74.5158 123.38C90.1958 131.98 106.196 140.95 117.646 153.85H31.3759C42.8259 140.95 58.8359 131.98 74.5059 123.38H74.5158ZM21.9959 166.85H127.026C133.436 177.85 135.436 187.97 133.116 197.56C132.606 199.67 131.876 201.76 130.956 203.85H18.0758C17.1458 201.76 16.4258 199.67 15.9158 197.56C13.5958 187.97 15.5959 177.85 22.0059 166.85H21.9959ZM18.4758 334.85C19.5758 327.73 21.5058 320.31 24.9158 312.85H124.106C127.516 320.31 129.456 327.73 130.546 334.85H18.4658H18.4758ZM116.546 299.85H32.4858C40.9758 288.05 54.1358 276.55 74.5158 266.43C94.8958 276.55 108.056 288.05 116.546 299.85ZM74.5158 249.76C52.2258 238.81 36.3658 227.92 26.6358 216.85H122.386C112.666 227.92 96.7959 238.81 74.5059 249.76H74.5158Z",
viewBox: "0 0 160 380",
fill: true,
},
bridge: {
path: "M280.54 25.28C294.52 17.73 301.18 11.42 301.85 10.76L291.4 0C291 0.38 250.66 37.89 149.61 37.89C58.66 37.89 9.68996 0.840001 9.20996 0.470001L0 12.31C0.87 12.99 9.96998 19.93 27.04 27.89V85.12H10.79V100.12H27.04V148.89H42.04V100.12H265.54V148.89H280.54V100.12H296.79V85.12H280.54V25.28ZM146.29 85.13H97.29V48.85C112 51.2 128.34 52.72 146.29 52.88V85.13ZM161.29 52.73C179.61 52.22 195.9 50.52 210.29 48.09V85.13H161.29V52.74V52.73ZM42.04 34.24C53.26 38.54 66.68 42.73 82.29 46.05V85.12H42.04V34.24ZM225.29 85.13V45.16C241.46 41.54 254.79 37.03 265.54 32.47V85.13H225.29Z",
viewBox: "0 0 302 149",
},
wheel: {
path: "M305.5 0C264.34 0 213.51 58.76 197.5 78.62C181.49 58.76 130.66 0 89.5 0C40.15 0 0 40.15 0 89.5C0 138.85 40.15 179 89.5 179C130.66 179 181.49 120.24 197.5 100.38C213.51 120.24 264.34 179 305.5 179C354.85 179 395 138.85 395 89.5C395 40.15 354.85 0 305.5 0ZM15 89.5C15 57.56 35.2 30.26 63.5 19.69V159.31C35.2 148.74 15 121.44 15 89.5ZM78.5 163.18V15.82C82.09 15.29 85.76 15 89.5 15C100.38 15 112.92 20.82 125.5 29.46V149.54C112.92 158.18 100.38 164 89.5 164C85.76 164 82.09 163.72 78.5 163.18ZM140.5 138.02V40.99C159.44 56.97 176.92 76.76 187.02 89.51C176.93 102.26 159.45 122.05 140.5 138.03V138.02ZM207.98 89.5C218.07 76.75 235.55 56.96 254.5 40.98V138.01C235.56 122.03 218.08 102.24 207.98 89.49V89.5ZM269.5 149.54V29.46C282.08 20.82 294.62 15 305.5 15C309.24 15 312.91 15.28 316.5 15.82V163.18C312.91 163.71 309.24 164 305.5 164C294.62 164 282.08 158.18 269.5 149.54ZM331.5 159.31V19.69C359.8 30.26 380 57.56 380 89.5C380 121.44 359.8 148.74 331.5 159.31Z",
viewBox: "0 0 395 179",
}
}
interface ContentItem {
id: number
subtitle: string
description: string
shape: ShapeData
}
export default function MainHero() {
const [content, setContent] = useState<ContentItem[]>([])
const [currentIndex, setCurrentIndex] = useState(0)
// Fetch content from API
useEffect(() => {
let isMounted = true
let controller: AbortController | null = null
let timeoutId: NodeJS.Timeout | null = null
const fetchContent = async () => {
try {
controller = new AbortController()
timeoutId = setTimeout(() => {
if (controller) {
controller.abort()
}
}, 10000) // 10 second timeout
let response
try {
// Use Next.js API route instead of direct Strapi call
response = await fetch("/api/main-hero", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: controller.signal,
})
// Clear timeout on successful fetch
if (timeoutId) {
clearTimeout(timeoutId)
timeoutId = null
}
} catch (fetchError: any) {
// Clear timeout in case of error
if (timeoutId) {
clearTimeout(timeoutId)
timeoutId = null
}
if (fetchError.name === 'AbortError' || fetchError.name === 'AbortController') {
throw new Error("Request timeout: The server took too long to respond")
}
throw fetchError
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const contentType = response.headers.get("content-type")
if (!contentType || !contentType.includes("application/json")) {
throw new Error("Response is not JSON")
}
const json = await response.json().catch((parseError) => {
throw new Error(`Failed to parse JSON: ${parseError.message}`)
})
if (!isMounted) return
if (json && json.data && Array.isArray(json.data)) {
// Map API data to component structure
// Assign shapes cyclically based on index
const shapeKeys = Object.keys(SHAPES)
if (shapeKeys.length === 0) {
console.warn("No shapes available")
setContent([])
return
}
const mappedContent = json.data
.filter((item: any) => item && item.id && item.sfssubtitle && item.sfsdescription)
.map((item: any, index: number) => {
try {
const shapeKey = shapeKeys[index % shapeKeys.length]
const shape = SHAPES[shapeKey]
if (!shape || !shape.path || !shape.viewBox) {
console.warn(`Invalid shape for key: ${shapeKey}`)
return null
}
return {
id: item.id,
subtitle: String(item.sfssubtitle || ""),
description: String(item.sfsdescription || ""),
shape: shape
}
} catch (mapError) {
console.error("Error mapping content item:", mapError)
return null
}
})
.filter((item: any) => item !== null) as ContentItem[]
if (mappedContent.length > 0 && isMounted) {
setContent(mappedContent)
} else if (isMounted) {
console.warn("No valid content items found after mapping")
setContent([])
}
} else if (isMounted) {
console.warn("Invalid API response structure", json)
setContent([])
}
} catch (error) {
if (isMounted) {
console.error("Failed to fetch hero content:", error)
setContent([])
}
}
}
// Execute fetch and ensure all errors are caught
const promise = fetchContent()
// Always attach a catch handler to prevent unhandled rejections
promise.catch((error) => {
// Only update state if component is still mounted
if (isMounted) {
console.error("Error in fetchContent:", error)
// Use setTimeout to ensure state update happens even if component is unmounting
setTimeout(() => {
if (isMounted) {
try {
setContent([])
} catch (e) {
// Ignore setState errors during unmount
}
}
}, 0)
}
})
return () => {
isMounted = false
// Cleanup: abort any pending requests and clear timeout
if (controller) {
controller.abort()
controller = null
}
if (timeoutId) {
clearTimeout(timeoutId)
timeoutId = null
}
}
}, [])
// Cycle content
useEffect(() => {
if (content.length === 0) return
const timer = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % content.length)
}, 4500)
return () => clearInterval(timer)
}, [content])
if (content.length === 0) {
return (
<section className="relative w-full bg-[#EAEAEA] py-6 md:py-10 lg:py-16 overflow-hidden min-h-[360px] lg:min-h-[600px] flex items-center justify-center">
{/* Loading state or initial empty state */}
</section>
)
}
const currentContent = content[currentIndex]
if (!currentContent) {
return (
<section className="relative w-full bg-[#EAEAEA] py-6 md:py-10 lg:py-16 overflow-hidden min-h-[360px] lg:min-h-[600px] flex items-center justify-center">
{/* Loading state or initial empty state */}
</section>
)
}
return (
<section className="relative w-full bg-[#EAEAEA] py-6 md:py-10 lg:py-16 overflow-hidden min-h-[360px] lg:min-h-[600px] flex items-center justify-center">
<div className="w-full max-w-[1200px] mx-auto pl-4 pr-2 sm:pl-6 sm:pr-3 lg:pl-8 lg:pr-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-0 items-center">
{/* --- Left Column: Text --- */}
<div className="flex flex-col justify-center text-center lg:text-left z-10 lg:pl-12">
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold text-[#353535] leading-tight">
Learning
</h1>
{/* Fixed Height Text Container to prevent jumping */}
<div className="relative h-[88px] md:h-[100px] lg:h-[110px]">
<AnimatePresence mode="wait">
<motion.div
key={currentContent.id}
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="absolute inset-0 flex flex-col justify-start"
>
<p className="text-3xl md:text-4xl lg:text-5xl italic text-[#353535] mb-2">
{currentContent.subtitle}
</p>
<div className="text-sm md:text-base text-[#111] leading-relaxed max-w-lg mx-auto lg:mx-0">
<p>{currentContent.description}</p>
</div>
</motion.div>
</AnimatePresence>
</div>
</div>
{/* --- Right Column: Animated Shape --- */}
<div className="relative flex items-center justify-center lg:justify-start lg:pl-7 lg:pr-2 h-[240px] md:h-[240px] lg:h-[400px]">
{/* Background Glow - Reduced horizontal space by 40% on large screens */}
<div className="absolute inset-y-0 inset-x-0 lg:inset-x-7 bg-gradient-radial from-[#f48120]/20 to-transparent opacity-60 blur-3xl" />
{/*
Square Container: enforced aspect-square with max dimensions.
Reduced by 40% for mobile/tablet, original size for desktop.
*/}
<div className="relative w-[192px] h-[192px] md:w-[240px] md:h-[240px] lg:w-[400px] lg:h-[400px] flex items-center justify-center p-4 md:p-5 lg:p-6">
<AnimatePresence mode="wait">
{currentContent.shape && currentContent.shape.path && currentContent.shape.viewBox ? (
<motion.div
key={`shape-${currentContent.id}`}
className="w-full h-full flex items-center justify-center"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.05 }}
transition={{ duration: 0.6, ease: "easeInOut" }}
>
{/* SVG Container */}
<svg
viewBox={currentContent.shape.viewBox}
className="w-full h-full text-[#F48120] drop-shadow-xl"
style={{
// Ensure the SVG scales uniformly within the square container
// "contain" behavior
width: "100%",
height: "100%",
overflow: "visible", // prevent clipping of stroke
}}
preserveAspectRatio="xMidYMid meet"
>
<motion.path
d={currentContent.shape.path}
fill="currentColor"
stroke="currentColor"
strokeWidth="1" // Minimal stroke since we are filling
strokeLinecap="round"
strokeLinejoin="round"
// Drawing Animation
initial={{ pathLength: 0, fillOpacity: 0 }}
animate={{
pathLength: 1,
fillOpacity: 1 // Solid fill for all shapes
}}
exit={{ pathLength: 0, fillOpacity: 0 }}
transition={{
duration: 0.8, // Faster animation to sync with text
ease: "easeInOut"
}}
/>
</svg>
</motion.div>
) : (
<div key={`fallback-${currentContent.id}`} className="w-full h-full flex items-center justify-center text-[#F48120]">
<p>Loading...</p>
</div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
</section>
)
}