// ============================================================ // sections-a.jsx — shared hooks, Nav, Hero, Team Snapshot // ============================================================ const { useState, useEffect, useRef, useCallback } = React; // ---- hooks ---- function useInView(opts = { threshold: 0, rootMargin: "0px 0px -12% 0px" }) { const ref = useRef(null); const [seen, setSeen] = useState(false); const [instant, setInstant] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; // Fallback: if already within the viewport at mount, reveal immediately, with no fade dependency. const r = el.getBoundingClientRect(); if (r.top < (window.innerHeight || document.documentElement.clientHeight) && r.bottom > 0) { setInstant(true); setSeen(true); return; } const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setSeen(true); io.disconnect(); } }, opts); io.observe(el); return () => io.disconnect(); }, []); return [ref, seen, instant]; } function Reveal({ children, delay = 0, as: Tag = "div", className = "", ...rest }) { const [ref, seen, instant] = useInView(); return ( {children} ); } function useCountUp(target, run, dur = 1500) { const [val, setVal] = useState(0); useEffect(() => { if (!run) return; let raf, start; const tick = (t) => { if (!start) start = t; const p = Math.min((t - start) / dur, 1); const e = 1 - Math.pow(1 - p, 3); setVal(target * e); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [run, target]); return val; } const GLYPHS = "ABCDEF0123456789#$%&/<>{}*"; function useScramble(text, run, speed = 38) { const [out, setOut] = useState(text); useEffect(() => { if (!run) return; let frame = 0; const total = text.length * 3; let raf; const step = () => { const revealed = frame / 3; setOut(text.split("").map((c, i) => { if (c === " ") return " "; if (i < revealed) return c; return GLYPHS[Math.floor(Math.random() * GLYPHS.length)]; }).join("")); frame++; if (frame <= total) raf = setTimeout(step, speed); else setOut(text); }; step(); return () => clearTimeout(raf); }, [run, text]); return out; } function Avatar({ m, size = 40 }) { const initials = m.name.split(" ").map(w => w[0]).slice(0, 2).join(""); // Deterministic hue from the member id/name so colours are stable across renders. const seed = (m.id || m.name).split("").reduce((s, c) => s + c.charCodeAt(0), 0); const h = 168 + (seed % 42); return (
{initials}
); } // ============================================================ // NAV // ============================================================ function Nav({ onJoin }) { const [scrolled, setScrolled] = useState(false); const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 40); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); const links = [["Leaderboards", "leaderboard"], ["Attendance", "attendance"], ["Activity", "feed"], ["Who we are", "manifesto"], ["Badges", "badges"], ["Story", "story"], ["Stats", "stats"]] .filter(([, id]) => SHOW_BADGES || id !== "badges"); return (
{menuOpen && ( )}
); } // ============================================================ // HERO // ============================================================ function Hero({ onJoin, figure = true }) { const [ref, seen] = useInView({ threshold: 0.1 }); const line2 = useScramble("CYBERSECURITY PROFESSIONALS", seen); // Ticker cycles through the real recent-activity feed. const [tick, setTick] = useState(0); useEffect(() => { if (FEED_DATA.length < 2) return; const iv = setInterval(() => setTick(t => (t + 1) % FEED_DATA.length), 3800); return () => clearInterval(iv); }, []); const ticker = FEED_DATA[tick] || null; return (
WHO WE ARE

WE ARE {line2} WHO RUN_

By day we build and secure applications, hunt threats, and defend organizations. By night, we run, chasing the same discipline that builds great systems & security.

{ticker && (
LIVE_FEED {ticker.who} {ticker.action}
)}
); } // ============================================================ // TEAM SNAPSHOT // ============================================================ function StatCounter({ value, decimals = 0 }) { const [ref, seen] = useInView(); const v = useCountUp(value, seen, 1600); return {v.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}; } function Snapshot() { const stats = [ { k: "Total distance", v: TEAM.distance, unit: "km", big: true, code: "DIST" }, { k: "Total runs", v: TEAM.runs, unit: "sessions", code: "RUNS" }, { k: "Total elevation", v: TEAM.elevation, unit: "m climbed", code: "ELEV" }, { k: "Active members", v: TEAM.members, unit: "runners", code: "MBRS" }, { k: "Running hours", v: TEAM.hours, unit: "hours", code: "TIME" }, ]; return (
// 05 — Team snapshot

The collective output,
updated every 15 minutes.

{stats.map((s, i) => (
{s.code}
{s.unit}
{s.k}
))}
); } Object.assign(window, { useInView, Reveal, useCountUp, useScramble, Avatar, Nav, Hero, Snapshot, StatCounter });