// 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 */}

{ e.target.style.display = 'none'; }} />
{badge && game.featured &&
Featured}
{/* body */}
{game.genre}
{game.title}
{game.description}
);
}
// ── 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 }) => (
);
return (
);
}
// ── 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 });