/* ============================================================ 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 (
No {metric.label.toLowerCase()} recorded yet
Stats appear once this weekend's scores are in.
No rounds logged yet
Your scoring mix appears after your first scored round.