import React, { useEffect, useMemo, useRef, useState } from "react"; /** * VJL4 Finals Bracket Simulator (React, system fonts, black/red/white theme) * ------------------------------------------------------------------------ * Fix for "SyntaxError: /: Unexpected token (1:0)": * - The Canvas file was of type code/react but contained a full HTML document. * - Replaced with a proper React component export so the runtime parses JS/JSX, not HTML. * * Notes * - 100% client-side; works on Cloudflare Pages. * - System font stack only (no web fonts). * - Responsive layout (1–4 columns depending on width). * - Confetti surprise when a Grand Final winner is picked (reduced‑motion aware). * * Seeds are taken from the final VJL4 ladder after Round 18. */ // ---------- Pure bracket helpers (tested below) ---------- export function computeParticipants(seeds, picks) { // seeds: {1..5: team} // picks: { qf, ef, ms, sf, pf, gf } = winners user selected const qf = { a: seeds[2], b: seeds[3], winner: picks.qf || null }; const ef = { a: seeds[4], b: seeds[5], winner: picks.ef || null }; const qfLoser = qf.winner ? qf.winner === seeds[2] ? seeds[3] : seeds[2] : null; const ms = { a: seeds[1], b: qf.winner || null, winner: picks.ms || null }; const sf = { a: qfLoser || null, b: ef.winner || null, winner: picks.sf || null }; const msLoser = ms.winner && ms.a && ms.b ? (ms.winner === ms.a ? ms.b : ms.a) : null; const pf = { a: msLoser || null, b: sf.winner || null, winner: picks.pf || null }; const gf = { a: ms.winner || null, b: pf.winner || null, winner: picks.gf || null }; return { qf, ef, ms, sf, pf, gf }; } export function clearDownstream(picks, changedKey) { const out = { ...picks }; if (changedKey === "qf") out.ms = out.sf = out.pf = out.gf = null; if (changedKey === "ef") out.sf = out.pf = out.gf = null; if (changedKey === "ms") out.pf = out.gf = null; if (changedKey === "sf") out.pf = out.gf = null; if (changedKey === "pf") out.gf = null; return out; } // ---------- UI bits ---------- const theme = { black: "#111111", red: "#e11d48", redDark: "#b91c1c", border: "#e5e7eb", }; function Card({ children, className = "" }) { return (
{children}
); } function SectionTitle({ children }) { return (

{children}

); } function TeamButton({ team, selected, disabled, onClick }) { const isPlaceholder = !team; const classes = [ "w-full text-center select-none rounded-xl px-3 py-2 border font-semibold transition", ]; if (selected) { classes.push("text-white"); } return ( ); } function MatchCard({ title, a, b, winner, onPick, disabled }) { return (
{title}
onPick(a)} /> onPick(b)} />
); } function SeedsBar({ seeds }) { return (
{[1, 2, 3, 4, 5].map((s) => (
Seed {s}
{seeds[s]}
))}
); } function HeaderBar() { return (
U14 Boys VJL4
); } function Controls({ onAuto, onReset }) { return (
); } // ---------- Confetti (no libs) ---------- function useConfetti() { const canvasRef = useRef(null); const rafRef = useRef(null); const particlesRef = useRef([]); const timerRef = useRef(null); const start = () => { const prefersReduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (prefersReduced) { // Minimal flash effect document.body.animate([{ background: "rgba(255,255,255,0)" }, { background: "rgba(255,255,255,.6)" }, { background: "rgba(255,255,255,0)" }], { duration: 800, easing: "ease", }); return; } const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }; resize(); const colors = ["#000000", "#ffffff", theme.red]; particlesRef.current = Array.from({ length: 240 }).map(() => ({ x: Math.random() * canvas.width, y: -10 - Math.random() * 50, vx: (Math.random() - 0.5) * 2.5, vy: 2 + Math.random() * 3, size: 4 + Math.random() * 6, rot: Math.random() * Math.PI, vr: (Math.random() - 0.5) * 0.2, color: colors[Math.floor(Math.random() * colors.length)], shape: Math.random() < 0.5 ? "rect" : "tri", })); const draw = (p) => { ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.fillStyle = p.color; if (p.shape === "rect") { ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size); } else { ctx.beginPath(); ctx.moveTo(0, -p.size / 1.2); ctx.lineTo(-p.size / 2, p.size / 2); ctx.lineTo(p.size / 2, p.size / 2); ctx.closePath(); ctx.fill(); } ctx.restore(); }; const loop = () => { rafRef.current = requestAnimationFrame(loop); ctx.clearRect(0, 0, canvas.width, canvas.height); particlesRef.current.forEach((p) => { p.x += p.vx; p.y += p.vy; p.rot += p.vr; if (p.y > canvas.height + 20) { p.y = -10; p.x = Math.random() * canvas.width; } draw(p); }); }; canvas.style.display = "block"; loop(); clearTimeout(timerRef.current); timerRef.current = setTimeout(() => stop(), 3500); const onResize = () => resize(); window.addEventListener("resize", onResize, { passive: true }); // cleanup hook when stopped const stop = () => { cancelAnimationFrame(rafRef.current); rafRef.current = null; particlesRef.current = []; ctx.clearRect(0, 0, canvas.width, canvas.height); canvas.style.display = "none"; window.removeEventListener("resize", onResize); }; // expose stop on ref for external call confettiStop.current = stop; }; const confettiStop = useRef(() => {}); const stop = () => confettiStop.current?.(); return { canvasRef, start, stop }; } // ---------- Tests ---------- function runTests() { const details = []; let pass = true; // Test 1: QF2 participants depend on QF1 winner { const seeds = { 1: "A", 2: "B", 3: "C", 4: "D", 5: "E" }; const picks = { qf: "B", ef: null, ms: null, sf: null, pf: null, gf: null }; const P = computeParticipants(seeds, picks); if (P.ms.a !== "A" || P.ms.b !== "B") { pass = false; details.push("Test1: QF2 (MS) should be A vs B"); } else { details.push("Test1: ✓ QF2 (MS) participants A vs B"); } } // Test 2: SF is L(QF1) vs W(EF) { const seeds = { 1: "A", 2: "B", 3: "C", 4: "D", 5: "E" }; const picks = { qf: "C", ef: "D", ms: null, sf: null, pf: null, gf: null }; const P = computeParticipants(seeds, picks); if (P.sf.a !== "B" || P.sf.b !== "D") { pass = false; details.push("Test2: SF should be B vs D"); } else { details.push("Test2: ✓ SF participants B vs D"); } } // Test 3: PF is L(MS) vs W(SF) { const seeds = { 1: "A", 2: "B", 3: "C", 4: "D", 5: "E" }; const picks = { qf: "B", ef: "D", ms: "A", sf: "D", pf: null, gf: null }; const P = computeParticipants(seeds, picks); if (P.pf.a !== "B" || P.pf.b !== "D") { pass = false; details.push("Test3: PF should be B vs D (L(MS)=B, W(SF)=D)"); } else { details.push("Test3: ✓ PF participants B vs D"); } } // Test 4: Changing QF clears downstream { const out = clearDownstream({ qf: "B", ef: "D", ms: "A", sf: "D", pf: "B", gf: "A" }, "qf"); if (out.ms !== null || out.sf !== null || out.pf !== null || out.gf !== null) { pass = false; details.push("Test4: Changing QF must clear MS/SF/PF/GF"); } else { details.push("Test4: ✓ Downstream cleared on QF change"); } } return { pass, details }; } function TestPanel() { const [res, setRes] = useState(null); useEffect(() => { setRes(runTests()); }, []); return (
Dev: tests
{res ? ( <>
{res.pass ? "All tests passed" : "Some tests failed"}
) : (
Running…
)}
); } // ---------- Main Component ---------- export default function App() { // Seeds from final ladder after Round 18 const SEEDS = useMemo( () => ({ 1: "Kilsyth 3", 2: "Chelsea", 3: "Camberwell", 4: "Korumburra", 5: "Kilsyth 2" }), [] ); const [picks, setPicks] = useState({ qf: null, ef: null, ms: null, sf: null, pf: null, gf: null }); const P = useMemo(() => computeParticipants(SEEDS, picks), [SEEDS, picks]); const { canvasRef, start: startConfetti, stop: stopConfetti } = useConfetti(); function setPick(key, winner) { setPicks((prev) => clearDownstream({ ...prev, [key]: prev[key] === winner ? null : winner }, key)); } function autoSim() { const rnd = (a, b) => (Math.random() < 0.55 ? a : b); const next = { qf: null, ef: null, ms: null, sf: null, pf: null, gf: null }; next.qf = rnd(SEEDS[2], SEEDS[3]); next.ef = rnd(SEEDS[4], SEEDS[5]); const msA = SEEDS[1]; const msB = next.qf; // requires QF winner next.ms = rnd(msA, msB); const qfLoser = next.qf === SEEDS[2] ? SEEDS[3] : SEEDS[2]; next.sf = rnd(qfLoser, next.ef); const msLoser = next.ms === msA ? msB : msA; next.pf = rnd(msLoser, next.sf); next.gf = rnd(next.ms, next.pf); setPicks(next); } function resetAll() { stopConfetti(); setPicks({ qf: null, ef: null, ms: null, sf: null, pf: null, gf: null }); } // trigger confetti when GF winner is set useEffect(() => { if (picks.gf) startConfetti(); }, [picks.gf]); return (

U14 Boys VJL4 Finals Bracket Simulator (2025)

Finals commence on Friday, 22 August 2025. Click teams to advance.
setPick("qf", t)} /> setPick("ef", t)} />
setPick("ms", t)} disabled={!P.ms.a || !P.ms.b} /> setPick("sf", t)} disabled={!P.sf.a || !P.sf.b} />
setPick("pf", t)} disabled={!P.pf.a || !P.pf.b} />
setPick("gf", t)} disabled={!P.gf.a || !P.gf.b} />
Champion: {picks.gf || "TBD"}
{/* Confetti Canvas */}
); }