/* global React */ // ========== CAROUSEL ========== const { useState: useStateC, useEffect: useEffectC } = React; const CAROUSEL_SLIDES = [ { tag: 'THE PRIVACY SERIES', title: "Nobody's", italic: 'watching', rest: 'your screen.', desc: '160° blackout. 9H tempered. Zero clarity loss head-on.', cta: 'Shop Privacy — ₹2,098', href: '/category/screens', img: 'assets/privacy-iphone-transparent.png', transparent: true, bg: 'var(--ink)', accent: 'var(--gold)', fg: 'var(--cream)' }, { tag: 'DROP-TESTED', title: 'Built to', italic: 'outlast', rest: 'your phone.', desc: 'Certified 3m drops on tile. Glass takes the hit. You keep scrolling.', cta: 'Explore Arc Edge — ₹1,049', href: '/category/screens', img: 'assets/ar-screen-transparent.png', transparent: true, bg: 'var(--violet)', accent: 'var(--gold)', fg: 'var(--cream)' }, ]; function Carousel() { const [i, setI] = useStateC(0); useEffectC(() => { const t = setInterval(() => setI(x => (x + 1) % CAROUSEL_SLIDES.length), 6000); return () => clearInterval(t); }, []); const s = CAROUSEL_SLIDES[i]; // Contrast helpers — light accents take ink text; dark backdrops get a cream disc behind product photo const LIGHT_ACCENTS = ['var(--gold)', 'var(--cream)', 'var(--cream-2)']; const DARK_BGS = ['var(--ink)', 'var(--ink-2)', 'var(--violet)', 'var(--violet-2)']; const onAccent = LIGHT_ACCENTS.includes(s.accent) ? 'var(--ink)' : 'var(--cream)'; const isDarkBg = DARK_BGS.includes(s.bg); return (
◆ {s.tag}

{s.title}
{s.italic}
{s.rest}

{s.desc}

{s.cta}
{/* Controls */}
{CAROUSEL_SLIDES.map((_, k) => (
); } // ========== MID BANNER (purple strip between sections) ========== function PurpleStrip() { return (
◆ FROM ₹799 · FREE SHIPPING

Power that keeps up with you.

Braided cables, GaN chargers, MagSafe powerbanks. Engineered for daily life, made in India.

Shop Power →
); } // ========== SECTION 04 — FULLSCREEN IMAGE SLIDER ========== // To add more images: just append { src: 'assets/yourfile.jpg', caption: '...' } // Works with ANY number of slides. // NO static fallback — banners are 100% admin-driven via /super-admin/banners. // If the API returns nothing, the carousel section just doesn't render at all. const IMAGE_SLIDES_FALLBACK = []; function ImageCarousel() { const [i, setI] = useStateC(0); const [IMAGE_SLIDES, setSlides] = useStateC(IMAGE_SLIDES_FALLBACK); const [loaded, setLoaded] = useStateC(false); // Auto-discover whatever's in /public/assets/banners/. No renaming needed — // any .webp/.jpg/.png/.avif files in the folder show up here. useEffectC(() => { fetch('/api/home-banners') .then(r => r.ok ? r.json() : null) .then(d => { if (Array.isArray(d) && d.length) setSlides(d); setLoaded(true); }) .catch(() => { setLoaded(true); }); }, []); const n = IMAGE_SLIDES.length; // ALL hooks must run on every render (React Rules of Hooks). The actual // "should we render" decision happens AFTER all useEffect calls below. useEffectC(() => { if (n <= 1) return; const t = setInterval(() => setI(x => (x + 1) % n), 7000); return () => clearInterval(t); }, [n]); // Now safe to early-return: hooks have all been called. if (!loaded) return null; if (!n) return null; const pad = (x) => String(x).padStart(2, '0'); return (
{/* Section tag — top-left */} {/* Slides */} {IMAGE_SLIDES.map((s, k) => { const bgs = ['var(--ink)', 'var(--violet)', 'var(--cream-2)', 'var(--steel)', 'var(--gold)']; const fgs = ['var(--cream)', 'var(--cream)', 'var(--ink)', 'var(--cream)', 'var(--ink)']; return (
{s.src ? ( {s.caption { e.currentTarget.style.display = 'none'; }} style={{ // cover = fill the banner edge-to-edge, no letterboxing. // onError hides the broken-image icon if the banner file // isn't uploaded yet — the dark background shows instead. width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center' }}/> ) : (
+
◆ PLACEHOLDER {pad(k + 1)}
{s.caption || `Image ${pad(k + 1)}`}
DROP YOUR IMAGE IN assets/
)}
); })} {/* Bottom chrome overlay */}
{IMAGE_SLIDES.map((_, k) => (
); } Object.assign(window, { Carousel, PurpleStrip, ImageCarousel });