// VEXEL — Shared UI atoms + header + footer // Loaded after data.js, before page files. const { useState, useEffect, useRef, useMemo, Fragment } = React; // ─── Logo (real artwork) ──────────────────────────────────────────────────── function Logo({ size = 28 }) { return ( Clutchscan ); } function Wordmark({ size = 22 }) { return (
VEXEL
); } // ─── Game logo glyph (CSS only) ────────────────────────────────────────────── function GameGlyph({ game, size = 32 }) { const g = typeof game === "string" ? gameById(game) : game; if (!g) return null; return (
{g.icon}
); } // ─── Team logo glyph ───────────────────────────────────────────────────────── function TeamLogo({ team, size = 40 }) { const t = typeof team === "string" ? teamById(team) : team; if (!t) return null; return (
{t.tag.slice(0, 4)}
); } // ─── Flag glyph ────────────────────────────────────────────────────────────── function Flag({ code, size = 16 }) { const f = REGION_FLAGS[code] || REGION_FLAGS.INT; return (
); } // ─── Live dot ──────────────────────────────────────────────────────────────── function LiveBadge({ children = "LIVE" }) { return ( {children} ); } // ─── Countdown timer ───────────────────────────────────────────────────────── function Countdown({ targetDays = 7 }) { const [t, setT] = useState({ d: targetDays, h: 14, m: 22, s: 47 }); useEffect(() => { const id = setInterval(() => { setT(prev => { let { d, h, m, s } = prev; s -= 1; if (s < 0) { s = 59; m -= 1; } if (m < 0) { m = 59; h -= 1; } if (h < 0) { h = 23; d -= 1; } if (d < 0) { d = 0; } return { d, h, m, s }; }); }, 1000); return () => clearInterval(id); }, []); const pad = (n) => String(n).padStart(2, "0"); return (
{[["DAYS", t.d], ["HRS", t.h], ["MIN", t.m], ["SEC", t.s]].map(([l, v]) => (
{pad(v)}
{l}
))}
); } // ─── Animated counter ──────────────────────────────────────────────────────── function NumberTicker({ value, prefix = "", suffix = "", duration = 1400 }) { const [n, setN] = useState(0); useEffect(() => { const start = performance.now(); let raf; const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setN(Math.round(eased * value)); if (t < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [value]); return {prefix}{n.toLocaleString("en-US")}{suffix}; } // ─── Search overlay ────────────────────────────────────────────────────────── function SearchOverlay({ open, onClose, go }) { const [q, setQ] = useState(""); const ref = useRef(); useEffect(() => { if (open) setTimeout(() => ref.current && ref.current.focus(), 50); }, [open]); if (!open) return null; const ql = q.toLowerCase(); const tournaments = TOURNAMENTS.filter(t => t.name.toLowerCase().includes(ql)).slice(0, 4); const teams = TEAMS.filter(t => t.name.toLowerCase().includes(ql) || t.tag.toLowerCase().includes(ql)).slice(0, 4); const players = PLAYERS.filter(p => p.handle.toLowerCase().includes(ql) || p.realName.toLowerCase().includes(ql)).slice(0, 4); return (
e.stopPropagation()}>
setQ(e.target.value)} style={{ padding: "20px 24px 20px 56px", fontSize: 18, background: "var(--surface)", borderColor: "var(--cyan)" }} />
{q && (
{[["Tournaments", tournaments, (t) => go("tournament", { id: t.id }), (t) => t.name], ["Teams", teams, (t) => go("team", { id: t.id }), (t) => t.name], ["Players", players, (p) => go("player", { id: p.id }), (p) => `${p.handle} · ${p.realName}`] ].map(([label, list, navFn, render]) => list.length ? (
{label}
{list.map((it, i) => (
{ navFn(it); onClose(); }} style={{ padding: "10px 12px", borderRadius: 4, cursor: "pointer", display: "flex", alignItems: "center", gap: 10 }} onMouseEnter={e => e.currentTarget.style.background = "var(--surface-3)"} onMouseLeave={e => e.currentTarget.style.background = "transparent"}> {render(it)}
))}
) : null)}
)}
); } // ─── Social icons (SVG, no emojis) ────────────────────────────────────────── function SocialIcon({ name, size = 16 }) { const props = { width: size, height: size, viewBox: "0 0 24 24", fill: "currentColor", style: { display: "block" } }; if (name === "twitter") return ; if (name === "discord") return ; if (name === "twitch") return ; if (name === "youtube") return ; if (name === "instagram") return ; if (name === "tiktok") return ; return null; } // ─── Newsletter form in footer ─────────────────────────────────────────────── function FooterNewsletter() { const [email, setEmail] = useState(""); const [status, setStatus] = useState("idle"); // idle | sending | ok | err const [msg, setMsg] = useState(""); const onSubmit = async (e) => { e.preventDefault(); setStatus("sending"); const r = await window.VexelAPI.newsletter(email, "footer"); if (r.ok) { setStatus("ok"); setEmail(""); } else { setStatus("err"); setMsg(r.error || "Please try again."); } }; if (status === "ok") { return
You're on the list. First letter ships Friday.
; } return (
setEmail(e.target.value)} style={{ borderRadius: "4px 0 0 4px" }} /> {status === "err" &&
{msg}
}
); } // ─── Header ────────────────────────────────────────────────────────────────── function Header({ route, go, signedIn, setSignedIn, lang, setLang, currency, setCurrency }) { const [searchOpen, setSearchOpen] = useState(false); const [menu, setMenu] = useState(false); const liveCount = TOURNAMENTS.filter(t => t.status === "live").length; const liveTickers = TOURNAMENTS.filter(t => t.status === "live").map(t => `${t.name} · ${gameById(t.game).name} · ${fmtAED(t.prize)}`); const tickers = [...liveTickers, ...liveTickers, ...liveTickers]; const navLinks = [ ["Tournaments", "tournaments"], ["Live", "live"], ["Teams", "teams"], ["Players", "players"], ["News", "news"], ["Store", "store"], ["Partners", "partners"], ]; return (
{/* Top utility strip */}
LIVE NOW · {liveCount} MATCHES
{tickers.map((t, i) => ( {t} ))}
{/* Main nav */}
go("home")} style={{ cursor: "pointer" }}>
{/* lang */}
{["EN", "AR"].map(l => ( ))}
{/* currency */}
{["AED", "USD"].map(c => ( ))}
{signedIn ? ( <> ) : ( <> )}
setSearchOpen(false)} go={go} />
); } // ─── Footer ────────────────────────────────────────────────────────────────── function Footer({ go }) { const cols = [ ["Platform", [["Tournaments", "tournaments"], ["Live matches", "live"], ["Teams", "teams"], ["Rankings", "players"], ["Store", "store"]]], ["Community", [ ["The Drop (News)", "news"], ["Partners", "partners"], ["Discord", { url: "https://discord.gg/vexel" }], ["X / Twitter", { url: "https://x.com/vexel" }], ["Twitch", { url: "https://twitch.tv/vexel" }] ]], ["Company", [ ["About", "about"], ["Careers", "careers"], ["Contact", "contact"], ["Press kit", "press"], ["Trade licence", "about"] ]], ["Help", [ ["FAQ", "faq"], ["Anti-cheat policy", "anticheat"], ["Code of conduct", "conduct"], ["Privacy", "privacy"], ["Terms", "terms"] ]], ]; return ( ); } // ─── Cookie banner ─────────────────────────────────────────────────────────── function CookieBanner() { const [open, setOpen] = useState(() => !localStorage.getItem("vexel-cookies")); if (!open) return null; const dismiss = () => { localStorage.setItem("vexel-cookies", "1"); setOpen(false); }; return (
Cookies. Probably not the kind you want.

We use them to keep matches in sync, remember your favourite teams, and learn what the GCC scene actually watches.

); } Object.assign(window, { Logo, Wordmark, GameGlyph, TeamLogo, Flag, LiveBadge, Countdown, NumberTicker, Header, Footer, CookieBanner });