// components.jsx — shared UI for Gamingo Labs const { useState, useEffect, useRef } = React; // ── Icons ───────────────────────────────────────────────────────────────────── const Ic = { Menu: (p) => , X: (p) => , Play: (p) => , Star: (p) => , Arrow: (p) => , Mail: (p) => , Chev: (p) => , }; // ── Star row ────────────────────────────────────────────────────────────────── function Stars({ v = 0, size = 13 }) { const n = Math.round(v); return ( {[1,2,3,4,5].map(i => )} ); } // ── Game Card ───────────────────────────────────────────────────────────────── function GameCard({ game, badge, goto }) { const handleClick = () => { if (goto) goto('game', { id: game.id }); else window.open(game.playStoreUrl, '_blank'); }; return (
{/* art */}
{game.title} { e.target.style.display = 'none'; }} /> {badge && game.featured && Featured}
{/* body */}
{game.genre}
{game.title}

{game.description}

{game.rating} ({game.downloads})
e.stopPropagation()}> Play
); } // ── Blog Card ───────────────────────────────────────────────────────────────── function BlogCard({ post, onClick }) { return (
{post.image && e.target.style.display='none'} />}
{post.category}
{post.title}

{post.excerpt}

{glDate(post.published_at || post.date)} Read →
); } // ── Service Card ────────────────────────────────────────────────────────────── function ServiceCard({ s }) { const [hov, setHov] = useState(false); return (
setHov(true)} onMouseLeave={() => setHov(false)}>
{s.icon}
{s.title}

{s.description}

); } // ── Eyebrow + Title block ───────────────────────────────────────────────────── function Eyebrow({ text }) { return
{text}
; } function SHead({ eye, title, sub, accent, center }) { const parts = accent ? title.split(new RegExp(`(${accent})`, 'i')) : [title]; return (

{parts.map((p, i) => p.toLowerCase() === (accent || '').toLowerCase() ? {p} : p )}

{sub &&

{sub}

}
); } // ── Toast ───────────────────────────────────────────────────────────────────── function Toast({ msg, clear }) { useEffect(() => { if (!msg) return; const t = setTimeout(clear, 3000); return () => clearTimeout(t); }, [msg]); if (!msg) return null; return (
{msg}
); } // ── Header ──────────────────────────────────────────────────────────────────── function Header({ page, goto }) { const [scrolled, setScrolled] = useState(false); const [mob, setMob] = useState(false); useEffect(() => { const fn = () => setScrolled(window.scrollY > 20); window.addEventListener('scroll', fn, { passive: true }); return () => window.removeEventListener('scroll', fn); }, []); const LINKS = [ { id: 'home', label: 'Home' }, { id: 'games', label: 'Games' }, { id: 'about', label: 'About' }, { id: 'services', label: 'Services' }, { id: 'blog', label: 'Blog' }, { id: 'videos', label: 'Videos' }, { id: 'players', label: 'Player Hub' }, { id: 'press', label: 'Press Kit' }, ]; const navLink = (id, label) => ( ); return (
{/* Logo */} {/* Desktop nav */}
{/* Mobile menu */} {mob && (
{LINKS.map(l => navLink(l.id, l.label))}
)}
); } // ── Footer ──────────────────────────────────────────────────────────────────── function Footer({ data, goto }) { const studio = data?.studio || GL_DATA.studio; const games = data?.games || GL_DATA.games; const year = new Date().getFullYear(); const [email, setEmail] = useState(''); const [done, setDone] = useState(false); const subscribe = async () => { if (!email.includes('@')) return; try { const r = await fetch('/api/newsletter.php?action=subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }) }); const j = await r.json(); if (j.success || j.ok) { setDone(true); return; } } catch {} setDone(true); // graceful fallback }; const Col = ({ title, links }) => (
{title}
); return (
{/* Brand */}

{studio.footerDesc}

Stay in the loop
{done ? ( ▶ You're in! ) : (
setEmail(e.target.value)} onKeyDown={e => e.key === 'Enter' && subscribe()} placeholder="your@email.com" className="gl-input" style={{ background: 'rgba(255,255,255,.06)', border: '1px solid rgba(255,255,255,.1)', color: '#fff', flex: 1 }} />
)}
goto('home') }, { label: 'About', fn: () => goto('about') }, { label: 'Services', fn: () => goto('services') }, { label: 'Blog', fn: () => goto('blog') }, { label: 'Videos', fn: () => goto('videos') }, { label: 'Press Kit', fn: () => goto('press') }, ]} /> ({ label: g.title.split('–')[0].trim(), href: g.playStoreUrl, ext: true }))} /> goto('contact') }, ].filter(Boolean)} />
© {studio.founded || 2020}–{year} {studio.name}. All rights reserved. Made with 🎮
); } // ── Marquee ─────────────────────────────────────────────────────────────────── function Marquee() { const items = ['Mobile Gaming','Google Play','Carrom City','Pool City','Screwdle','Puzzle Games','Board Games','Sports Games','Online Multiplayer','Indie Dev','Game Publishing','Gamingo Labs']; const all = [...items, ...items]; return (
{all.map((it, i) => {it} )}
); } // ── Page progress bar ───────────────────────────────────────────────────────── function ProgressBar() { const [w, setW] = useState(0); useEffect(() => { setW(20); const t1 = setTimeout(() => setW(70), 200); const t2 = setTimeout(() => setW(100), 600); const t3 = setTimeout(() => setW(0), 950); return () => { clearTimeout(t1); clearTimeout(t2); clearTimeout(t3); }; }, []); if (!w) return null; return
; } // ── Newsletter CTA band ─────────────────────────────────────────────────────── function NLBand({ studio }) { const [email, setEmail] = useState(''); const [done, setDone] = useState(false); const sub = async () => { if (!email.includes('@')) return alert('Enter a valid email'); try { const r = await fetch('/api/newsletter.php?action=subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }) }); const j = await r.json(); if (j.success || j.ok) { setDone(true); return; } } catch {} setDone(true); }; return (

STAY IN THE LOOP

{studio?.newsletterDesc || 'Game launches, dev updates and tips from the team.'}

{done ?
▶ You're in! Game updates incoming.
:
setEmail(e.target.value)} onKeyDown={e => e.key === 'Enter' && sub()} placeholder="your@email.com" className="gl-input" style={{ flex: 1, minWidth: 200 }} />
}
); } // ── Theme Switcher ──────────────────────────────────────────────────────────── const GL_THEMES = { obsidian: { name: 'Obsidian', sub: 'Dark · Green', file: 'theme-obsidian.css', preview: ['#050507','#00ff88','#f8f8ff'] }, midnight: { name: 'Midnight', sub: 'Dark · Violet', file: 'theme-midnight.css', preview: ['#06060e','#a855f7','#f0eeff'] }, ember: { name: 'Ember', sub: 'Dark · Orange', file: 'theme-ember.css', preview: ['#080503','#ff6b2b','#fff8f0'] }, arctic: { name: 'Arctic', sub: 'Light · Blue', file: 'theme-arctic.css', preview: ['#f8faff','#0066ff','#0c1222'] }, }; function injectTheme(key) { const theme = GL_THEMES[key]; if (!theme) return; const existing = document.getElementById('gl-theme-link'); if (existing) existing.remove(); const link = document.createElement('link'); link.id = 'gl-theme-link'; link.rel = 'stylesheet'; link.href = '/' + theme.file; document.head.appendChild(link); try { localStorage.setItem('gl_theme', key); } catch {} } function ThemeSwitcher() { const [open, setOpen] = useState(false); const [active, setActive] = useState(() => { try { return localStorage.getItem('gl_theme') || 'obsidian'; } catch { return 'obsidian'; } }); const pick = (key) => { injectTheme(key); setActive(key); setOpen(false); }; return (
{open && (
Theme
{Object.entries(GL_THEMES).map(([key, t]) => ( ))}
Saved to your browser
)}
); } Object.assign(window, { Ic, Stars, GameCard, BlogCard, ServiceCard, Eyebrow, SHead, Toast, Header, Footer, Marquee, ProgressBar, NLBand, ThemeSwitcher, GL_THEMES, injectTheme });