// ============================================================
// 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 (
);
}
// ============================================================
// 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.
Connect Strava
{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}
{Array.from({ length: 16 }).map((_, j) => (
))}
))}
);
}
Object.assign(window, { useInView, Reveal, useCountUp, useScramble, Avatar, Nav, Hero, Snapshot, StatCounter });