/* Leaderboard.jsx — 英雄榜 · HALL OF HEROES (public, read-only) - tabs: 貢獻值榜 / 武勳值榜 - top-3 podium (壹/貳/叁), then rows 04–N - reads from `sw_board_data_v1` localStorage if admin has run OCR (otherwise uses static seed) */ const LS_BOARD_KEY = "sw_board_data_v1"; const SEED = [ { pid: "SW-09712340", name: "傾城劍主", contribution: 12680, merit: 4210, job: "zhen", jobCN: "鎮軍", role: "盟 主" }, { pid: "SW-09301284", name: "墨染長安", contribution: 8420, merit: 3540, job: "qi", jobCN: "奇佐", role: "軍 師" }, { pid: "SW-08842197", name: "雪夜赤刃", contribution: 7950, merit: 3380, job: "shen", jobCN: "神行", role: "副 盟" }, { pid: "SW-09421008", name: "月下孤舟", contribution: 7210, merit: 3120, job: "qing", jobCN: "青囊", role: "醫 正" }, { pid: "SW-07710043", name: "破曉行者", contribution: 6840, merit: 2980, job: "si", jobCN: "司倉", role: "倉 令" }, { pid: "SW-09180277", name: "青衫不語", contribution: 6120, merit: 2810, job: "tian", jobCN: "天工", role: "工 正" }, { pid: "SW-08823341", name: "山河入夢", contribution: 5780, merit: 2640, job: "zhen", jobCN: "鎮軍", role: "前 鋒" }, { pid: "SW-09603392", name: "逐月之刃", contribution: 5340, merit: 2510, job: "qi", jobCN: "奇佐", role: "幕 僚" }, { pid: "SW-08407715", name: "霜降寒江", contribution: 4920, merit: 2380, job: "shen", jobCN: "神行", role: "斥 候" }, { pid: "SW-08881040", name: "執燈夜行", contribution: 4680, merit: 2240, job: "qing", jobCN: "青囊", role: "醫 師" }, ]; const fmt = (n) => n.toLocaleString("en-US"); const ordCN = ["壹","貳","叁"]; const Leaderboard = () => { const [tab, setTab] = React.useState("contrib"); // load from admin-synced localStorage if available const [data, setData] = React.useState(() => { try { const raw = localStorage.getItem(LS_BOARD_KEY); if (!raw) return { rows: SEED, updatedAt: null, updatedBy: null, source: null }; const parsed = JSON.parse(raw); if (parsed?.rows?.length) return parsed; } catch {} return { rows: SEED, updatedAt: null, updatedBy: null, source: null }; }); // Live-sync: refresh on storage event (cross-tab) + poll React.useEffect(() => { const refresh = () => { try { const raw = localStorage.getItem(LS_BOARD_KEY); if (!raw) return; const parsed = JSON.parse(raw); if (parsed?.rows?.length) setData(parsed); } catch {} }; const onStorage = (e) => { if (e.key === LS_BOARD_KEY) refresh(); }; window.addEventListener("storage", onStorage); const t = setInterval(refresh, 8000); return () => { window.removeEventListener("storage", onStorage); clearInterval(t); }; }, []); const lastUpdatedLabel = data.updatedAt ? new Date(data.updatedAt).toLocaleString("zh-Hant", { hour12: false }).replace(/\//g, "-") : "2026-05-19 13:42:11"; const updatedByLabel = data.source || "管理員手動輸入"; const sorted = React.useMemo(() => { const k = tab === "contrib" ? "contribution" : "merit"; return [...data.rows].sort((a, b) => (b[k] || 0) - (a[k] || 0)); }, [data, tab]); const metric = tab === "contrib" ? "contribution" : "merit"; const metricCN = tab === "contrib" ? "貢獻值" : "武勳值"; const top3 = sorted.slice(0, 3); return (
SEASON 3 · WEEK 07 · 截至本週更新 · UPDATED {lastUpdatedLabel}
{/* Top-3 podium */}
{[1, 0, 2].map((i) => { const row = top3[i]; if (!row) return
; const variant = i === 0 ? "gold" : i === 1 ? "silver" : "bronze"; return (
{i === 0 && Champion · 盟 首} {ordCN[i]} {row.name} {fmt(row[metric])}
); })}
{/* Rows 4..N */}
顯示前 {top3.length} 名 · 排序依「{metricCN}」高低 · 數據來源 {updatedByLabel}
); }; window.Leaderboard = Leaderboard;