/* ============================================================ Dashboard Leaderboard — a colorful, sortable season summary every member sees on their dashboard. Draws on the SAME data as the Season tab (posted weekends + the live current weekend) via YCC_SCORING.weekendStats / weekendWins. Pure-CSS/SVG charts. ============================================================ */ // Scoring-outcome palette: good (gold/green) → bad (amber/red). const MIX = [ { key: "eagles", label: "Eagle", color: "#7a5a22" }, { key: "birdies", label: "Birdie", color: "#b4904e" }, { key: "pars", label: "Par", color: "#3a7a4f" }, { key: "bogeys", label: "Bogey", color: "#6f8f7a" }, { key: "doubles", label: "Double", color: "#c2743e" }, { key: "bogeyPlus", label: "Bgy+", color: "#9e3b2f" }, ]; // Aggregate every member's season stats (posted weekends + live weekend). function aggregateSeason(ctx) { const Y = window.YCC, ENG = window.YCC_SCORING; const { seasonDoc, accountMembers, tour, scores, sideResults, gameConfig, member } = ctx; const fullName = (m) => `${m.first_name || ""} ${m.last_name || ""}`.trim() || m.email; const postedStats = (seasonDoc && seasonDoc.stats) || {}; const live = ENG.weekendStats ? ENG.weekendStats({ tour, scores, sideResults, gameConfig }) : {}; const NUM = ["eagles", "birdies", "pars", "bogeys", "doubles", "bogeyPlus", "holes", "strokes", "toPar", "rounds", "ctp", "longDrive", "wins"]; const agg = {}; const blank = () => { const o = { weekendsWon: 0 }; NUM.forEach((k) => (o[k] = 0)); return o; }; const add = (byName) => Object.entries(byName || {}).forEach(([name, s]) => { const a = agg[name] || (agg[name] = blank()); NUM.forEach((k) => { a[k] += (s[k] || 0); }); if ((s.wins || 0) > 0) a.weekendsWon += 1; }); Object.values(postedStats).forEach(add); const liveIncluded = !postedStats[tour.satISO]; if (liveIncluded) add(live); const hiByName = {}; (accountMembers || []).forEach((m) => { const n = fullName(m); if (m.handicap_index != null) hiByName[n] = m.handicap_index; }); if (member && member.name && member.handicapIndex != null && hiByName[member.name] == null) hiByName[member.name] = member.handicapIndex; const names = new Set(); (accountMembers || []).forEach((m) => names.add(fullName(m))); Object.keys(agg).forEach((n) => names.add(n)); if (member && member.name) names.add(member.name); const rows = Array.from(names).map((name) => { const a = agg[name] || blank(); const avg = a.holes ? Math.round((a.strokes / a.holes) * 18) : null; return { name, hi: hiByName[name] != null ? hiByName[name] : null, rounds: a.rounds, eagles: a.eagles, birdies: a.birdies, pars: a.pars, bogeys: a.bogeys, doubles: a.doubles, bogeyPlus: a.bogeyPlus, be: a.birdies + a.eagles, avg, ctp: a.ctp, longDrive: a.longDrive, wins: a.wins, weekendsWon: a.weekendsWon, holes: a.holes, played: a.holes > 0, }; }); return { rows, liveIncluded, weeks: Object.keys(postedStats).length }; } const LB_METRICS = [ { key: "wins", label: "Wins", icon: "trophy", desc: "Game wins" }, { key: "avg", label: "Scoring", icon: "target", lowGood: true, desc: "Avg per 18 holes" }, { key: "eagles", label: "Eagles", icon: "star", desc: "Eagles" }, { key: "birdies", label: "Birdies", icon: "star", desc: "Birdies" }, { key: "pars", label: "Pars", icon: "flag", desc: "Pars made" }, { key: "bogeys", label: "Bogeys", icon: "flag", desc: "Bogeys" }, { key: "doubles", label: "Doubles", icon: "flag", desc: "Double bogeys" }, { key: "ctp", label: "Closest", icon: "pin", desc: "Closest-to-pin wins" }, { key: "longDrive", label: "Long Dr", icon: "trendUp", desc: "Longest-drive wins" }, ]; const MEDALS = ["#cba23b", "#aab1ad", "#b9824c"]; // gold, silver, bronze function initialsOf(n) { return (n || "?").trim().split(/\s+/).map((s) => s[0]).join("").slice(0, 2).toUpperCase(); } function DashboardLeaderboard({ ctx }) { const { member, go } = ctx; const me = member && member.name; const { rows, liveIncluded, weeks } = React.useMemo(() => aggregateSeason(ctx), [ctx]); const [metricKey, setMetricKey] = React.useState("wins"); const [grow, setGrow] = React.useState(false); const metric = LB_METRICS.find((m) => m.key === metricKey) || LB_METRICS[0]; // re-animate bars whenever the metric changes React.useEffect(() => { setGrow(false); const t = setTimeout(() => setGrow(true), 40); return () => clearTimeout(t); }, [metricKey]); const val = (r) => r[metric.key]; // only rank members who have a value for this metric const ranked = rows .map((r) => ({ ...r, v: val(r) })) .filter((r) => r.v != null && (metric.lowGood ? r.played : r.v > 0)) .sort((a, b) => metric.lowGood ? a.v - b.v : b.v - a.v || a.name.localeCompare(b.name)); const maxV = ranked.length ? Math.max(...ranked.map((r) => r.v)) : 0; const minV = ranked.length ? Math.min(...ranked.map((r) => r.v)) : 0; // bar fraction 0..1 — for lowGood, lower is a fuller bar const frac = (v) => { if (metric.lowGood) { if (maxV === minV) return 1; return 0.25 + 0.75 * ((maxV - v) / (maxV - minV)); } return maxV ? v / maxV : 0; }; const myRank = ranked.findIndex((r) => r.name === me); const myRow = rows.find((r) => r.name === me); // podium = top 3, rendered tallest (#1) on the LEFT → shortest on the right const podium = ranked.slice(0, 3); const podiumOrder = podium; const fmtVal = (r) => metric.lowGood ? (r.v == null ? "—" : r.v) : r.v; return (
{/* header */}

Season leaderboard

{weeks} posted weekend{weeks === 1 ? "" : "s"}{liveIncluded ? " · incl. this weekend live" : ""}
{/* metric selector — cream chip strip, matching the Round history layout */}
{LB_METRICS.map((m) => { const on = m.key === metricKey; return ( ); })}
{/* body */}
{/* left: podium + bars */}
{ranked.length === 0 ? (

No {metric.label.toLowerCase()} recorded yet

Stats appear once this weekend's scores are in.

) : ( <> {/* podium */} {podium.length >= 2 && (
{podiumOrder.map((r) => { const place = podium.indexOf(r); // 0,1,2 (left→right, #1 first) // height scales with the value, so equal values = equal bars const h = Math.round(72 + frac(r.v) * 60); const mine = r.name === me; return (
{place + 1} {place === 0 && }
{r.name.split(" ")[0]}{mine ? " (you)" : ""}
{fmtVal(r)}
); })}
)} {/* ranked bars */}
{ranked.slice(0, 8).map((r, i) => { const mine = r.name === me; const pct = Math.max(4, Math.round(frac(r.v) * 100)); return (
{i + 1}
{r.name}{mine ? " (you)" : ""} {fmtVal(r)}
); })}
)}
{/* right: your season */}
Your season
); } function YourSeason({ myRow, myRank, metric, fieldCount }) { const played = myRow && myRow.played; const mixTotal = played ? MIX.reduce((n, m) => n + (myRow[m.key] || 0), 0) : 0; // donut geometry const R = 52, C = 2 * Math.PI * R, sw = 16; let offset = 0; const segs = played && mixTotal ? MIX.map((m) => { const v = myRow[m.key] || 0; const len = (v / mixTotal) * C; const seg = { color: m.color, dash: len, gap: C - len, off: -offset, label: m.label, v }; offset += len; return seg; }).filter((s) => s.v > 0) : []; const rankText = myRank >= 0 ? `#${myRank + 1}` : "—"; return (
{/* rank + metric */}
{rankText}
{metric.label} rank
{myRank >= 0 ? <>of {fieldCount} ranked · {metric.lowGood ? (myRow ? myRow[metric.key] : "—") : (myRow ? myRow[metric.key] : 0)} {metric.lowGood ? "avg" : metric.label.toLowerCase()} : "Not ranked yet for this metric"}
{/* scoring mix donut */}
Scoring mix
{played && mixTotal ? (
{segs.map((s, i) => ( ))}
{myRow.holes}
holes
{MIX.map((m) => { const v = myRow[m.key] || 0; if (!v) return null; return (
{m.label} {v}
); })}
) : (

No rounds logged yet

Your scoring mix appears after your first scored round.

)} {/* quick stat chips */} {played && (
{[["Wins", myRow.wins, "trophy"], ["Birdies+", myRow.be, "star"], ["Rounds", myRow.rounds, "flag"]].map(([lab, v, ic]) => (
{v}
{lab}
))}
)}
); } Object.assign(window, { DashboardLeaderboard, aggregateSeason });