);
}
// Site-wide Steam sign-in control. Identity is a signed session cookie set by
// /api/auth/callback only after Steam confirms it signed the OpenID assertion —
// there is no client-side auth state, and no way to browse as someone else.
function SteamAuth() {
const me = MK.SESSION;
const [open, setOpen] = React.useState(false);
const ref = React.useRef(null);
React.useEffect(() => { const h = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, []);
const signIn = () => { location.href = 'api/auth/login'; };
const signOut = () => { fetch('api/auth/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {}).then(() => location.reload()); };
const steam = ;
if (!me) return ;
return (
);
}
function MkFooter() {
const linkCss = {fontFamily:'var(--mk-font-mono)',fontSize:11,fontWeight:600,letterSpacing:'0.16em',textTransform:'uppercase',textDecoration:'none',color:'var(--mk-violet)'};
return (
);
}
/* Approximate ping.
A browser cannot open a UDP socket, so the real in-game latency is out of
reach — the number the imported design showed was Math.random() jitter around
a constant. What is measurable: an empty HTTPS request to the game hostname,
which is a DNS-only record on the club's own connection, so the round trip
covers the same internet path the game traffic takes.
Warm up first (that request pays DNS + TCP + TLS), then take the fastest of
three samples — the minimum is the one least polluted by scheduling noise.
responseStart - requestStart is the request/response pair on its own, and is
readable cross-origin only because /mk-ping sends Timing-Allow-Origin.
undefined = still measuring, null = unavailable. Never a made-up number. */
const PING_NOTE = 'approximate — timed over HTTPS from your browser to the server’s network. Your in-game ping will differ.';
function hostOf(addr) { return String(addr || '').split(':')[0]; }
async function measurePing(host) {
const url = n => 'https://' + host + '/mk-ping?n=' + n;
const opts = { cache: 'no-store', mode: 'cors', credentials: 'omit' };
try { await fetch(url('warm'), opts); } catch (e) { return null; }
const samples = [];
for (let i = 0; i < 3; i++) {
const u = url(i + '-' + Math.random().toString(36).slice(2));
const t0 = performance.now();
try { await fetch(u, opts); } catch (e) { return null; }
const wall = performance.now() - t0;
const e = performance.getEntriesByName(u).pop();
samples.push(e && e.responseStart && e.requestStart ? e.responseStart - e.requestStart : wall);
}
try { performance.clearResourceTimings(); } catch (e) {}
return Math.max(1, Math.round(Math.min.apply(null, samples)));
}
/* Keyed by host, not by server: every room on one box shares one round trip. */
function useApproxPing(servers) {
const [pings, setPings] = React.useState({});
const hosts = Array.from(new Set((servers || []).filter(s => s.online).map(s => hostOf(s.addr)))).sort().join(',');
React.useEffect(() => {
let live = true;
(hosts ? hosts.split(',') : []).forEach(h => {
measurePing(h).then(ms => { if (live) setPings(p => Object.assign({}, p, { [h]: ms })); });
});
return () => { live = false; };
}, [hosts]);
return pings;
}
function PingText({ ms, style }) {
const txt = ms === undefined ? '…' : ms === null ? '—' : '≈' + ms + ' ms';
return {txt};
}
function StatusDot({ online }) {
return
{online ? 'online' : 'offline'}
;
}
/* Two different connect paths, and they do not accept the same thing. Steam's
steam://connect/ URL handler needs a numeric address (server.connect, built
from the resolved IP by the API); the in-game console resolves DNS, so the
copied string keeps the hostname and survives a WAN IP change. */
function ConnectActions({ server, size }) {
const [copied, setCopied] = React.useState(false);
const signal = () => { try { mkChime(); } catch (e) {} window.dispatchEvent(new CustomEvent('mk-connect')); };
const copy = () => {
navigator.clipboard.writeText('connect ' + server.addr).catch(()=>{});
signal();
setCopied(true); setTimeout(() => setCopied(false), 1600);
};
return (
);
}
const mkMono = {fontFamily:'var(--mk-font-mono)',fontSize:12,fontWeight:600,letterSpacing:'0.12em',textTransform:'uppercase'};
// Marks the parts of the site that are NOT reading real data. Player stats and
// standings come from the K4-System tables and server status from A2S; the
// CounterStrikeSharp admin tables aren't installed, so moderation stays mock
// and says so rather than passing itself off.
function DemoNote({ children, style }) {
return (
);
}
// Tempo marking from how busy the club is right now (an-empty-room to packed).
function tempoMark(players) {
if (players <= 0) return 'Tacet';
if (players < 8) return 'Adagio';
if (players < 18) return 'Andante';
if (players < 30) return 'Allegro';
return 'Presto';
}
// A five-note motif on connect — one soft note per key of the stripe.
function mkChime() {
try {
const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return;
const ctx = mkChime._ctx || (mkChime._ctx = new AC());
if (ctx.state === 'suspended') ctx.resume();
const now = ctx.currentTime;
[523.25, 587.33, 659.25, 783.99, 1046.5].forEach((f, i) => {
const o = ctx.createOscillator(), g = ctx.createGain();
o.type = 'triangle'; o.frequency.value = f;
const t = now + i * 0.072;
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(0.12, t + 0.015);
g.gain.exponentialRampToValueAtTime(0.0008, t + 0.34);
o.connect(g).connect(ctx.destination);
o.start(t); o.stop(t + 0.38);
});
} catch (e) {}
}
// The five-key stripe as a living heartbeat: tempo scales with players online,
// and every connect (Play / copy) ripples the keys via the 'mk-connect' event.
function TempoStripe({ players = 0, height = 4, gap = 5, style }) {
const dur = Math.max(2.0, 3.8 - Math.min(players, 40) / 40 * 1.8);
const [flash, setFlash] = React.useState(false);
React.useEffect(() => {
let id;
const on = () => { setFlash(true); clearTimeout(id); id = setTimeout(() => setFlash(false), 660); };
window.addEventListener('mk-connect', on);
return () => { window.removeEventListener('mk-connect', on); clearTimeout(id); };
}, []);
return (
{[0,1,2,3,4].map(i => )}
);
}
function useReveal(once) {
const ref = React.useRef(null);
const [seen, setSeen] = React.useState(false);
React.useEffect(() => {
const el = ref.current; if (!el) return;
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { setSeen(true); if (once) io.disconnect(); }
else if (!once) setSeen(false);
}, { threshold: 0.15 });
io.observe(el); return () => io.disconnect();
}, []);
return [ref, seen];
}
function Reveal({ children, delay = 0, once, as = 'div', className = '', style }) {
const [ref, seen] = useReveal(once);
return React.createElement(as, { ref, className: ('mk-reveal ' + (seen ? 'in ' : '') + className).trim(), style: { animationDelay: delay + 's', ...style } }, children);
}
/* Who's connected, from A2S. The protocol reports score and time only — no
deaths, so there is no k/d to show — and no SteamID, so a row links to a
member page only when the display name matches one. */
function Roster({ server }) {
const list = (MK.ROSTERS[server.id] || []).slice().sort((a, b) => b.score - a.score);
const known = Object.fromEntries((MK.PLAYERS || []).map(p => [p.name, p.slug]));
const rGrid = {display:'grid',gridTemplateColumns:'2fr .8fr .9fr',gap:8,padding:'8px 20px',alignItems:'center'};
const since = m => m >= 60 ? Math.floor(m / 60) + 'h ' + (m % 60) + 'm' : m + 'm';
if (!server.online) return