Files
OpenFUT-Bridge/src/dashboard.html
T
funman300 ba1b0a09f2 Phase 17: level-up toast, XP progress bar, multi-squad management
Match result display now shows a level-up row per level gained (new
level, coins granted, milestone pack) and fires a toast notification.

Settings/Profile card now uses xp_to_next_level and xp_for_next_level
from the enhanced GET /profile response so the XP bar accurately shows
progress within the current level rather than total XP %.

Squad View mode gains an All Squads panel below Chemistry listing every
saved squad (name + formation badge). Each row has a Delete button that
calls DELETE /squads/:id and refreshes the view. Squads are loaded via
GET /squads alongside the active squad on every Squad tab open.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 17:42:35 -07:00

2083 lines
102 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenFUT — Club Dashboard</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #0d1117; color: #c9d1d9; line-height: 1.5; }
/* ── Header ── */
header { background: #161b22; border-bottom: 1px solid #30363d; padding: 12px 24px;
display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
header h1 { font-size: 1.1rem; color: #f5a623; }
.club-summary { margin-left: auto; display: flex; gap: 24px; align-items: center; font-size: 0.9rem; }
.coins { color: #f5a623; font-weight: 700; font-size: 1.1rem; }
.manager { color: #8b949e; }
.badge { font-size: 0.75rem; background: #238636; color: #fff; padding: 2px 8px; border-radius: 12px; }
/* ── Nav tabs ── */
nav { background: #161b22; border-bottom: 1px solid #30363d; padding: 0 24px; display: flex; gap: 0; overflow-x: auto; }
nav button { background: none; border: none; color: #8b949e; padding: 12px 18px; cursor: pointer;
font-size: 0.9rem; border-bottom: 2px solid transparent; white-space: nowrap; }
nav button:hover { color: #c9d1d9; }
nav button.active { color: #f5a623; border-bottom-color: #f5a623; }
/* ── Main ── */
main { padding: 24px; max-width: 1280px; margin: 0 auto; }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ── Cards ── */
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; margin-top: 16px; }
.player-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px;
position: relative; cursor: default; transition: border-color 0.15s; }
.player-card:hover { border-color: #f5a623; }
.card-ovr { font-size: 1.6rem; font-weight: 800; color: #f5a623; line-height: 1; }
.card-pos { font-size: 0.7rem; color: #8b949e; text-transform: uppercase; letter-spacing: 0.05em; }
.card-name { font-size: 0.85rem; font-weight: 600; margin: 4px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-meta { font-size: 0.72rem; color: #8b949e; }
.card-rarity { position: absolute; top: 8px; right: 8px; font-size: 0.65rem;
padding: 1px 5px; border-radius: 10px; }
.rarity-gold, .rarity-raregold { background: #78350f; color: #fcd34d; }
.rarity-silver { background: #374151; color: #d1d5db; }
.rarity-bronze { background: #451a03; color: #fbbf24; }
.rarity-totw, .rarity-icon, .rarity-hero { background: #312e81; color: #a5b4fc; }
.card-upgrades { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
.upgrade-pill { font-size: 0.65rem; padding: 1px 6px; border-radius: 10px; }
.chem-pill { background: #1c4532; color: #6ee7b7; }
.train-pill { background: #1e3a5f; color: #93c5fd; }
.loan-pill { background: #451a03; color: #fbbf24; }
/* ── Stat bars ── */
.stat-bar-row { display: flex; gap: 6px; align-items: center; font-size: 0.7rem; margin: 2px 0; }
.stat-bar-label { width: 28px; color: #8b949e; text-align: right; }
.stat-bar-track { flex: 1; height: 4px; background: #21262d; border-radius: 2px; }
.stat-bar-fill { height: 100%; border-radius: 2px; background: #3fb950; }
/* ── Filters ── */
.filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
.filters input, .filters select { background: #161b22; border: 1px solid #30363d;
color: #c9d1d9; padding: 6px 10px; border-radius: 6px; font-size: 0.85rem; }
.filters input:focus, .filters select:focus { outline: none; border-color: #f5a623; }
/* ── Packs ── */
.pack-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; margin-top: 16px; }
.pack-item { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
.pack-item h3 { font-size: 0.95rem; color: #f5a623; margin-bottom: 4px; }
.pack-item .pack-id { font-size: 0.7rem; color: #8b949e; word-break: break-all; margin-bottom: 12px; }
/* ── Objectives ── */
.obj-list { display: grid; gap: 10px; margin-top: 16px; }
.obj-item { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 14px; }
.obj-item.completed { border-color: #238636; }
.obj-item.claimed { border-color: #30363d; opacity: 0.6; }
.obj-header { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.obj-title { font-size: 0.9rem; font-weight: 600; }
.obj-reward { font-size: 0.8rem; color: #f5a623; }
.progress-track { height: 6px; background: #21262d; border-radius: 3px; margin-top: 8px; }
.progress-fill { height: 100%; border-radius: 3px; background: #1f6feb; }
.progress-fill.done { background: #238636; }
.obj-meta { font-size: 0.75rem; color: #8b949e; margin-top: 4px;
display: flex; justify-content: space-between; }
/* ── Division ── */
.division-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 24px; max-width: 480px; }
.division-num { font-size: 3rem; font-weight: 800; color: #f5a623; line-height: 1; }
.division-label { color: #8b949e; font-size: 0.85rem; margin-bottom: 16px; }
.record-row { display: flex; gap: 24px; margin: 12px 0; }
.record-cell { text-align: center; }
.record-cell .val { font-size: 1.4rem; font-weight: 700; }
.record-cell.w .val { color: #3fb950; }
.record-cell.d .val { color: #8b949e; }
.record-cell.l .val { color: #f85149; }
.record-cell .lbl { font-size: 0.7rem; color: #8b949e; }
/* ── FUT Champs ── */
.champs-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 24px; max-width: 560px; }
.tier-badge { display: inline-block; padding: 4px 14px; border-radius: 20px; font-weight: 700; font-size: 0.85rem;
background: #312e81; color: #a5b4fc; margin: 8px 0 16px; }
.tier-Elite { background: #78350f; color: #fcd34d; }
.tier-Gold { background: #713f12; color: #fcd34d; }
.tier-Silver { background: #374151; color: #d1d5db; }
.tier-Bronze { background: #451a03; color: #d97706; }
/* ── Notifications ── */
.notif-list { display: grid; gap: 8px; margin-top: 16px; }
.notif-item { background: #161b22; border-left: 3px solid #f5a623; border-radius: 0 6px 6px 0; padding: 12px 16px; }
.notif-item.loan_expiring { border-color: #d97706; }
.notif-item.season_ending { border-color: #1f6feb; }
.notif-item.objective_complete { border-color: #238636; }
.notif-title { font-size: 0.9rem; font-weight: 600; }
.notif-msg { font-size: 0.8rem; color: #8b949e; margin-top: 2px; }
/* ── Buttons ── */
.btn { display: inline-block; padding: 7px 14px; border-radius: 6px; border: none;
cursor: pointer; font-size: 0.85rem; font-weight: 500; }
.btn-primary { background: #f5a623; color: #000; }
.btn-primary:hover { background: #d4851a; }
.btn-secondary { background: #21262d; color: #c9d1d9; border: 1px solid #30363d; }
.btn-secondary:hover { background: #30363d; }
.btn-danger { background: #b91c1c; color: #fff; }
.btn-danger:hover { background: #991b1b; }
.btn-sm { padding: 4px 10px; font-size: 0.78rem; }
/* ── Misc ── */
.empty { color: #8b949e; font-size: 0.9rem; padding: 24px 0; }
.loading { color: #8b949e; font-size: 0.9rem; }
.error-msg { color: #f85149; font-size: 0.85rem; padding: 8px; background: #2d1515; border-radius: 4px; }
.row-actions { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
color: #8b949e; border-bottom: 1px solid #30363d; padding-bottom: 8px; margin-bottom: 16px; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
@media (max-width: 640px) { .two-col { grid-template-columns: 1fr; } }
.section { margin-bottom: 32px; }
.info-row { display: flex; gap: 8px; align-items: baseline; margin: 4px 0; font-size: 0.85rem; }
.info-row .lbl { color: #8b949e; width: 110px; flex-shrink: 0; }
.modal-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 100; align-items: center; justify-content: center; }
.modal-backdrop.open { display: flex; }
.modal { background: #161b22; border: 1px solid #30363d; border-radius: 10px; padding: 24px;
max-width: 480px; width: 90%; max-height: 80vh; overflow-y: auto; }
.modal h3 { color: #f5a623; margin-bottom: 16px; }
.opened-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; margin-top: 12px; }
/* ── Squad ── */
.squad-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; margin-top: 12px; }
.squad-slot { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 10px; }
.squad-slot .slot-pos { font-size: 0.7rem; text-transform: uppercase; color: #8b949e; margin-bottom: 4px; }
.chem-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 2px; }
.chem-dot.full { background: #3fb950; }
.chem-dot.partial { background: #d29922; }
.chem-dot.none { background: #f85149; }
.chem-bar { height: 8px; background: #21262d; border-radius: 4px; overflow: hidden; margin: 6px 0; }
.chem-bar-fill { height: 100%; border-radius: 4px; background: #3fb950; }
.formation-badge { font-size: 0.75rem; background: #1c3250; color: #93c5fd; padding: 2px 8px; border-radius: 10px; }
/* ── Draft ── */
.draft-candidates { display: grid; grid-template-columns: repeat(auto-fill, minmax(165px, 1fr)); gap: 10px; margin-top: 12px; }
.draft-candidate { background: #161b22; border: 2px solid #30363d; border-radius: 8px; padding: 10px;
cursor: pointer; transition: border-color 0.15s; }
.draft-candidate:hover { border-color: #f5a623; }
.pick-slots { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 8px; }
.pick-dot { width: 12px; height: 12px; border-radius: 50%; background: #21262d; border: 1px solid #30363d; }
.pick-dot.filled { background: #f5a623; border-color: #f5a623; }
.draft-progress { font-size: 0.8rem; color: #8b949e; margin-bottom: 12px; }
/* ── Market ── */
.market-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; margin-top: 12px; }
.market-item { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px; }
.market-price { color: #f5a623; font-weight: 700; font-size: 1rem; margin: 6px 0; }
.market-seller { font-size: 0.7rem; color: #8b949e; }
.sell-form { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; max-width: 480px; }
.sell-form select, .sell-form input { width: 100%; background: #0d1117; border: 1px solid #30363d;
color: #c9d1d9; padding: 8px 10px; border-radius: 6px; font-size: 0.85rem; margin: 6px 0 12px; }
/* ── Catalog ── */
.catalog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(175px, 1fr)); gap: 10px; margin-top: 12px; }
.owned-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 8px;
border: 2px solid #3fb950; pointer-events: none; }
.owned-tag { position: absolute; bottom: 6px; left: 6px; font-size: 0.62rem; background: #14532d;
color: #4ade80; padding: 1px 5px; border-radius: 8px; }
.catalog-count { font-size: 0.8rem; color: #8b949e; margin: 8px 0; }
/* ── Settings ── */
.settings-form { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; max-width: 480px; }
.settings-form label { font-size: 0.8rem; color: #8b949e; display: block; margin-bottom: 4px; }
.settings-form select, .settings-form input {
width: 100%; background: #0d1117; border: 1px solid #30363d; color: #c9d1d9;
padding: 8px 10px; border-radius: 6px; font-size: 0.85rem; margin-bottom: 14px; }
.profile-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; max-width: 480px; }
.xp-bar { height: 6px; background: #21262d; border-radius: 3px; margin: 6px 0 14px; }
.xp-fill { height: 100%; border-radius: 3px; background: #1f6feb; }
/* ── Squad Builder ── */
.builder-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; margin: 12px 0; }
.builder-slot { background: #21262d; border: 1px dashed #30363d; border-radius: 6px; padding: 8px;
min-height: 60px; cursor: pointer; transition: border-color 0.15s; position: relative; }
.builder-slot:hover { border-color: #f5a623; }
.builder-slot.filled { background: #161b22; border-style: solid; border-color: #30363d; }
.builder-slot.gk-slot { border-color: #1f6feb44; }
.slot-label { font-size: 0.68rem; color: #8b949e; text-transform: uppercase; margin-bottom: 4px; }
.slot-clear { position: absolute; top: 4px; right: 6px; font-size: 0.7rem; color: #f85149;
cursor: pointer; line-height: 1; }
/* ── Matches ── */
.match-form { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; max-width: 560px; }
.score-row { display: flex; align-items: center; gap: 12px; margin: 12px 0; }
.score-input { width: 64px; background: #0d1117; border: 1px solid #30363d; color: #f5a623;
font-size: 1.6rem; font-weight: 800; text-align: center; border-radius: 6px; padding: 4px; }
.score-sep { font-size: 1.4rem; color: #8b949e; }
.opponent-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 14px; margin: 12px 0; }
.opponent-rating { font-size: 2rem; font-weight: 800; color: #f5a623; }
.match-history-row { display: flex; align-items: center; gap: 10px; padding: 8px 0;
border-bottom: 1px solid #21262d; font-size: 0.85rem; }
.outcome-badge { width: 28px; height: 28px; border-radius: 4px; font-weight: 700; font-size: 0.75rem;
display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.outcome-win { background: #14532d; color: #4ade80; }
.outcome-draw { background: #374151; color: #d1d5db; }
.outcome-loss { background: #7f1d1d; color: #f87171; }
/* ── SBC ── */
.sbc-list { display: grid; gap: 12px; margin-top: 12px; }
.sbc-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
.sbc-card.completed { border-color: #238636; opacity: 0.7; }
.sbc-name { font-size: 0.95rem; font-weight: 600; margin-bottom: 4px; }
.sbc-desc { font-size: 0.8rem; color: #8b949e; margin-bottom: 10px; }
.sbc-reqs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.req-pill { font-size: 0.7rem; padding: 2px 8px; border-radius: 10px; background: #21262d; color: #c9d1d9; }
.sbc-reward { font-size: 0.8rem; color: #f5a623; margin-bottom: 10px; }
.sbc-picker { margin-top: 10px; }
.sbc-picker select { width: 100%; background: #0d1117; border: 1px solid #30363d; color: #c9d1d9;
padding: 6px; border-radius: 6px; font-size: 0.8rem; margin: 3px 0; }
.sbc-selected { display: flex; flex-wrap: wrap; gap: 4px; margin: 6px 0; min-height: 24px; }
.sbc-sel-pill { font-size: 0.7rem; background: #1c3250; color: #93c5fd; padding: 2px 8px;
border-radius: 10px; cursor: pointer; }
/* ── Events ── */
.events-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; margin-top: 12px; }
.event-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
.event-card.active { border-color: #f5a623; }
.event-name { font-size: 0.95rem; font-weight: 600; margin-bottom: 4px; }
.event-desc { font-size: 0.8rem; color: #8b949e; margin-bottom: 10px; }
.event-status { font-size: 0.75rem; font-weight: 600; }
.event-status.on { color: #3fb950; }
.event-status.off { color: #8b949e; }
</style>
</head>
<body>
<header>
<h1>⚽ OpenFUT Dashboard</h1>
<span class="badge">Offline</span>
<div class="club-summary">
<span class="manager" id="hdr-manager"></span>
<span id="hdr-club"></span>
<span class="coins" id="hdr-coins">— coins</span>
</div>
</header>
<nav>
<button class="active" onclick="showTab('club')">Club</button>
<button onclick="showTab('collection')">Collection</button>
<button onclick="showTab('packs')">Packs</button>
<button onclick="showTab('objectives')">Objectives</button>
<button onclick="showTab('division')">Division</button>
<button onclick="showTab('champs')">FUT Champs</button>
<button onclick="showTab('catalog')">Card Catalog</button>
<button onclick="showTab('settings')">Settings</button>
<button onclick="showTab('matches')">Matches</button>
<button onclick="showTab('store')">Pack Store</button>
<button onclick="showTab('sbc')">SBC</button>
<button onclick="showTab('statistics')">Statistics</button>
<button onclick="showTab('squad')">Squad</button>
<button onclick="showTab('draft')">Draft</button>
<button onclick="showTab('market')">Market</button>
<button onclick="showTab('events')">Events</button>
<button onclick="showTab('notifications')">Notifications <span id="notif-badge"></span></button>
</nav>
<main>
<!-- ── Club tab ────────────────────────────────────────────────────────────── -->
<div id="tab-club" class="tab-panel active">
<div class="two-col">
<div class="section">
<h2>Club Info</h2>
<div id="club-info"><span class="loading">Loading…</span></div>
<div class="row-actions" style="margin-top:16px">
<input id="edit-name" placeholder="Club name" style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:6px 10px;border-radius:6px;font-size:0.85rem">
<input id="edit-manager" placeholder="Manager name" style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:6px 10px;border-radius:6px;font-size:0.85rem">
<button class="btn btn-primary btn-sm" onclick="saveClub()">Save</button>
</div>
</div>
<div class="section">
<h2>Statistics</h2>
<div id="stats-info"><span class="loading">Loading…</span></div>
</div>
</div>
</div>
<!-- ── Collection tab ─────────────────────────────────────────────────────── -->
<div id="tab-collection" class="tab-panel">
<div class="filters">
<input id="f-name" placeholder="Name…" oninput="filterCards()">
<select id="f-pos" onchange="filterCards()">
<option value="">All positions</option>
<option>GK</option><option>CB</option><option>RB</option><option>LB</option>
<option>CDM</option><option>CM</option><option>CAM</option>
<option>RM</option><option>LM</option><option>RW</option><option>LW</option>
<option>CF</option><option>ST</option>
</select>
<select id="f-rarity" onchange="filterCards()">
<option value="">All rarities</option>
<option value="gold">Gold</option><option value="raregold">Rare Gold</option>
<option value="silver">Silver</option><option value="bronze">Bronze</option>
<option value="totw">TOTW</option><option value="icon">Icon</option>
</select>
<input id="f-club" placeholder="Club…" oninput="filterCards()">
<input id="f-nation" placeholder="Nation…" oninput="filterCards()">
<select id="f-sort" onchange="filterCards()">
<option value="ovr">Sort: OVR ↓</option>
<option value="name">Sort: Name</option>
<option value="pos">Sort: Position</option>
</select>
</div>
<div id="collection-count" style="font-size:0.8rem;color:#8b949e;margin-bottom:8px"></div>
<div id="collection-grid" class="card-grid"><span class="loading">Loading…</span></div>
</div>
<!-- ── Packs tab ─────────────────────────────────────────────────────────── -->
<div id="tab-packs" class="tab-panel">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px">
<h2 style="border:none;margin:0">Unopened Packs</h2>
<button class="btn btn-secondary btn-sm" onclick="loadPacks()">Refresh</button>
</div>
<div id="packs-grid" class="pack-grid"><span class="loading">Loading…</span></div>
<div class="section" style="margin-top:32px">
<h2>Pack History</h2>
<div id="pack-history"><span class="loading">Loading…</span></div>
</div>
</div>
<!-- ── Objectives tab ────────────────────────────────────────────────────── -->
<div id="tab-objectives" class="tab-panel">
<div id="objectives-list" class="obj-list"><span class="loading">Loading…</span></div>
</div>
<!-- ── Division tab ──────────────────────────────────────────────────────── -->
<div id="tab-division" class="tab-panel">
<div class="two-col">
<div>
<h2>Division Rivals</h2>
<div id="division-card"><span class="loading">Loading…</span></div>
<div style="margin-top:16px">
<button class="btn btn-primary" onclick="claimRivalsReward()">Claim Weekly Reward</button>
<p style="font-size:0.75rem;color:#8b949e;margin-top:6px">Offline mode: claim whenever you're ready to end your rivals week.</p>
</div>
</div>
<div>
<h2>Season Progress</h2>
<div id="season-progress"><span class="loading">Loading…</span></div>
</div>
</div>
</div>
<!-- ── FUT Champions tab ─────────────────────────────────────────────────── -->
<div id="tab-champs" class="tab-panel">
<div class="two-col">
<div>
<h2>Current Session</h2>
<div id="champs-current"><span class="loading">Loading…</span></div>
</div>
<div>
<h2>Past Sessions</h2>
<div id="champs-history"><span class="loading">Loading…</span></div>
</div>
</div>
</div>
<!-- ── Card Catalog tab ───────────────────────────────────────────────────── -->
<div id="tab-catalog" class="tab-panel">
<div class="filters">
<input id="cat-name" placeholder="Name…" oninput="filterCatalog()">
<select id="cat-pos" onchange="filterCatalog()">
<option value="">All positions</option>
<option>GK</option><option>CB</option><option>RB</option><option>LB</option>
<option>CDM</option><option>CM</option><option>CAM</option>
<option>RM</option><option>LM</option><option>RW</option><option>LW</option>
<option>CF</option><option>ST</option>
</select>
<select id="cat-rarity" onchange="filterCatalog()">
<option value="">All rarities</option>
<option value="gold">Gold</option><option value="raregold">Rare Gold</option>
<option value="silver">Silver</option><option value="bronze">Bronze</option>
<option value="totw">TOTW</option><option value="icon">Icon</option>
</select>
<input id="cat-nation" placeholder="Nation…" oninput="filterCatalog()">
<input id="cat-league" placeholder="League…" oninput="filterCatalog()">
<input id="cat-club" placeholder="Club…" oninput="filterCatalog()">
<input id="cat-min-ovr" type="number" placeholder="Min OVR" min="1" max="99" oninput="filterCatalog()" style="width:80px">
<input id="cat-max-ovr" type="number" placeholder="Max OVR" min="1" max="99" oninput="filterCatalog()" style="width:80px">
</div>
<div id="catalog-count" class="catalog-count"></div>
<div id="catalog-grid" class="catalog-grid"><span class="loading">Loading…</span></div>
<div style="margin-top:16px;text-align:center">
<button id="catalog-more-btn" class="btn btn-secondary" style="display:none" onclick="catalogLoadMore()">Load more</button>
</div>
</div>
<!-- ── Settings tab ───────────────────────────────────────────────────────── -->
<div id="tab-settings" class="tab-panel">
<div class="two-col">
<div class="section">
<h2>Profile</h2>
<div class="profile-card" id="profile-card"><span class="loading">Loading…</span></div>
</div>
<div class="section">
<h2>Game Settings</h2>
<div class="settings-form">
<label>Default match difficulty</label>
<select id="set-difficulty">
<option value="beginner">Beginner</option>
<option value="professional">Professional</option>
<option value="world_class">World Class</option>
<option value="legendary">Legendary</option>
<option value="ultimate">Ultimate</option>
</select>
<label>Preferred formation</label>
<select id="set-formation">
<option>4-4-2</option><option>4-3-3</option><option>4-2-3-1</option>
<option>4-1-2-1-2</option><option>3-5-2</option><option>5-3-2</option>
</select>
<button class="btn btn-primary btn-sm" onclick="saveSettings()">Save Settings</button>
<div id="settings-saved" style="font-size:0.8rem;color:#3fb950;margin-top:8px;display:none">✓ Saved</div>
</div>
</div>
</div>
</div>
<!-- ── Matches tab ───────────────────────────────────────────────────────── -->
<div id="tab-matches" class="tab-panel">
<div class="two-col">
<div>
<h2>Simulate Match</h2>
<div class="match-form">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
<label style="font-size:0.8rem;color:#8b949e">Difficulty:</label>
<select id="match-difficulty" style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:4px 8px;border-radius:6px;font-size:0.8rem">
<option value="beginner">Beginner</option>
<option value="professional" selected>Professional</option>
<option value="world_class">World Class</option>
<option value="legendary">Legendary</option>
<option value="ultimate">Ultimate</option>
</select>
<button class="btn btn-secondary btn-sm" onclick="generateOpponent()">Generate Opponent</button>
</div>
<div id="opponent-display"></div>
<div class="score-row">
<input id="goals-for" class="score-input" type="number" min="0" max="20" value="0">
<span class="score-sep"></span>
<input id="goals-against" class="score-input" type="number" min="0" max="20" value="0">
</div>
<div style="font-size:0.75rem;color:#8b949e;margin-bottom:12px">Your goals — Opponent goals</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-secondary btn-sm" onclick="quickScore(3,0)">Win 3-0</button>
<button class="btn btn-secondary btn-sm" onclick="quickScore(1,1)">Draw 1-1</button>
<button class="btn btn-secondary btn-sm" onclick="quickScore(0,2)">Loss 0-2</button>
</div>
<div style="margin-top:16px">
<button class="btn btn-primary" onclick="submitMatch()">Submit Result</button>
</div>
<div id="match-result-display" style="margin-top:14px"></div>
</div>
</div>
<div>
<h2>Match History</h2>
<div id="match-history"><span class="loading">Loading…</span></div>
</div>
</div>
</div>
<!-- ── Pack Store tab ─────────────────────────────────────────────────────── -->
<div id="tab-store" class="tab-panel">
<p style="font-size:0.85rem;color:#8b949e;margin-bottom:16px">
Purchase packs with your coins. Bought packs appear in the Packs tab ready to open.
</p>
<div id="store-grid" class="pack-grid"><span class="loading">Loading…</span></div>
</div>
<!-- ── SBC tab ────────────────────────────────────────────────────────────── -->
<div id="tab-sbc" class="tab-panel">
<p style="font-size:0.85rem;color:#8b949e;margin-bottom:16px">
Complete Squad Building Challenges to earn coins, XP, and packs. Cards submitted are consumed.
</p>
<div id="sbc-list" class="sbc-list"><span class="loading">Loading…</span></div>
</div>
<!-- ── Statistics tab ────────────────────────────────────────────────────── -->
<div id="tab-statistics" class="tab-panel">
<div class="two-col">
<div>
<h2>Career Stats</h2>
<div id="career-stats"><span class="loading">Loading…</span></div>
</div>
<div>
<h2>Goals by Position</h2>
<div id="position-goals"><span class="loading">Loading…</span></div>
</div>
</div>
<div class="section" style="margin-top:24px">
<h2>Recent Match Log</h2>
<div id="stats-history"><span class="loading">Loading…</span></div>
</div>
</div>
<!-- ── Squad tab ─────────────────────────────────────────────────────────── -->
<div id="tab-squad" class="tab-panel">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px;flex-wrap:wrap">
<h2 style="border:none;margin:0">Squad</h2>
<button class="btn btn-secondary btn-sm" id="squad-view-btn" onclick="squadMode('view')" style="border-color:#f5a623;color:#f5a623">View</button>
<button class="btn btn-secondary btn-sm" id="squad-build-btn" onclick="squadMode('build')">Build</button>
<button class="btn btn-secondary btn-sm" onclick="loadSquad()">Refresh</button>
</div>
<!-- View mode -->
<div id="squad-view-mode">
<div class="two-col">
<div>
<div id="squad-header"></div>
<div id="squad-grid" class="squad-grid"><span class="loading">Loading…</span></div>
</div>
<div>
<h2>Chemistry</h2>
<div id="squad-chemistry"><span class="loading">Loading…</span></div>
<div style="margin-top:24px">
<h2>All Squads</h2>
<div id="squad-list"><span class="loading">Loading…</span></div>
</div>
</div>
</div>
</div>
<!-- Build mode -->
<div id="squad-build-mode" style="display:none">
<div class="two-col" style="margin-bottom:16px">
<div>
<label style="font-size:0.8rem;color:#8b949e">Squad name</label>
<input id="builder-name" placeholder="My Squad" style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:6px 10px;border-radius:6px;font-size:0.85rem;width:100%;margin:4px 0 12px">
</div>
<div>
<label style="font-size:0.8rem;color:#8b949e">Formation</label>
<select id="builder-formation" onchange="rebuildSlots()" style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:6px 10px;border-radius:6px;font-size:0.85rem;width:100%;margin:4px 0 12px">
<option>4-4-2</option><option>4-3-3</option><option>4-2-3-1</option>
<option>4-1-2-1-2</option><option>3-5-2</option><option>5-3-2</option>
</select>
</div>
</div>
<h2>Starters (11)</h2>
<div id="builder-starters" class="builder-slots"></div>
<h2 style="margin-top:16px">Bench (up to 7)</h2>
<div id="builder-bench" class="builder-slots"></div>
<div style="margin-top:16px;display:flex;gap:8px">
<button class="btn btn-primary" onclick="saveSquad()">Save Squad</button>
<button class="btn btn-secondary" onclick="clearBuilder()">Clear All</button>
</div>
<!-- Mini card picker overlay -->
<div id="picker-overlay" style="display:none;margin-top:16px">
<div style="background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px;max-width:600px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<strong style="font-size:0.9rem">Pick player for slot <span id="picker-slot-label"></span></strong>
<button class="btn btn-secondary btn-sm" onclick="closePicker()">Cancel</button>
</div>
<input id="picker-search" placeholder="Filter by name…" oninput="renderPicker()"
style="background:#0d1117;border:1px solid #30363d;color:#c9d1d9;padding:6px 10px;border-radius:6px;font-size:0.85rem;width:100%;margin-bottom:10px">
<div id="picker-list" style="max-height:280px;overflow-y:auto;display:grid;gap:4px"></div>
</div>
</div>
</div>
</div>
<!-- ── Draft tab ──────────────────────────────────────────────────────────── -->
<div id="tab-draft" class="tab-panel">
<div class="two-col">
<div>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px;flex-wrap:wrap">
<h2 style="border:none;margin:0">FUT Draft</h2>
<select id="draft-difficulty" style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:4px 8px;border-radius:6px;font-size:0.8rem">
<option value="beginner">Beginner</option>
<option value="professional" selected>Professional</option>
<option value="world_class">World Class</option>
<option value="legendary">Legendary</option>
<option value="ultimate">Ultimate</option>
</select>
<button class="btn btn-primary btn-sm" id="draft-start-btn" onclick="startDraft()">New Draft</button>
<button class="btn btn-danger btn-sm" id="draft-abandon-btn" style="display:none" onclick="abandonDraft()">Abandon</button>
</div>
<div id="draft-status"></div>
<div id="draft-candidates" class="draft-candidates"></div>
</div>
<div>
<h2>Picks So Far</h2>
<div id="draft-picks" class="squad-grid"></div>
</div>
</div>
</div>
<!-- ── Market tab ────────────────────────────────────────────────────────── -->
<div id="tab-market" class="tab-panel">
<div class="section">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px;flex-wrap:wrap">
<h2 style="border:none;margin:0">Transfer Market</h2>
<button class="btn btn-secondary btn-sm" onclick="refreshMarket()">Refresh Listings</button>
<input id="m-search" placeholder="Search player…" oninput="filterMarket()"
style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 10px;border-radius:6px;font-size:0.8rem">
</div>
<div id="market-grid" class="market-grid"><span class="loading">Loading…</span></div>
</div>
<div class="section">
<h2>Sell a Card</h2>
<div class="sell-form">
<label style="font-size:0.8rem;color:#8b949e">Select card from collection</label>
<select id="sell-card-select"><option value="">— choose card —</option></select>
<label style="font-size:0.8rem;color:#8b949e">Buy-now price (coins)</label>
<input id="sell-price" type="number" min="100" step="50" value="1000" placeholder="Price">
<button class="btn btn-primary btn-sm" onclick="sellCard()">List for Sale</button>
</div>
<div class="section" style="margin-top:16px">
<h2>My Listings</h2>
<div id="my-listings"><span class="loading">Loading…</span></div>
</div>
</div>
</div>
<!-- ── Events tab ────────────────────────────────────────────────────────── -->
<div id="tab-events" class="tab-panel">
<p style="font-size:0.85rem;color:#8b949e;margin-bottom:16px">
Activate special events to unlock bonus cards in the market, score multipliers, and extra objectives.
All events are offline-only and have no expiry unless you deactivate them.
</p>
<div id="events-grid" class="events-grid"><span class="loading">Loading…</span></div>
</div>
<!-- ── Notifications tab ─────────────────────────────────────────────────── -->
<div id="tab-notifications" class="tab-panel">
<div id="notif-list" class="notif-list"><span class="loading">Loading…</span></div>
</div>
</main>
<!-- Pack open result modal -->
<div class="modal-backdrop" id="pack-modal">
<div class="modal">
<h3>Pack Opened!</h3>
<div id="modal-cards" class="opened-cards"></div>
<div style="margin-top:16px;text-align:right">
<button class="btn btn-secondary" onclick="closeModal()">Close</button>
</div>
</div>
</div>
<script>
const CORE = '{{CORE_URL}}';
let allCards = [];
// ── Utilities ────────────────────────────────────────────────────────────────
async function api(method, path, body) {
const opts = { method, headers: { 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
const r = await fetch(CORE + path, opts);
const j = await r.json().catch(() => ({ error: 'invalid JSON' }));
if (!r.ok) throw new Error(j.error || `HTTP ${r.status}`);
return j;
}
function el(id) { return document.getElementById(id); }
function showToast(msg, isError = false) {
const t = document.createElement('div');
t.textContent = msg;
t.style.cssText = `position:fixed;bottom:24px;right:24px;padding:10px 18px;border-radius:6px;
background:${isError ? '#7f1d1d' : '#14532d'};color:#fff;font-size:0.85rem;z-index:200;
box-shadow:0 4px 12px rgba(0,0,0,0.4)`;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3500);
}
function showTab(name) {
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('nav button').forEach(b => b.classList.remove('active'));
el('tab-' + name).classList.add('active');
event.target.classList.add('active');
({ club: loadClub, collection: loadCollection, packs: loadPacks,
objectives: loadObjectives, division: loadDivision,
champs: loadChamps, squad: loadSquad, draft: loadDraft,
market: loadMarket, events: loadEvents, matches: loadMatches,
store: loadStore, sbc: loadSbc, statistics: loadStatistics,
catalog: loadCatalog, settings: loadSettings,
notifications: loadNotifications })[name]?.();
}
function rarityClass(r) {
return 'rarity-' + (r || 'bronze').toLowerCase().replace(' ', '');
}
function statBar(label, val) {
const pct = Math.min(100, val);
const color = val >= 80 ? '#3fb950' : val >= 65 ? '#d29922' : '#f85149';
return `<div class="stat-bar-row">
<span class="stat-bar-label">${label}</span>
<div class="stat-bar-track"><div class="stat-bar-fill" style="width:${pct}%;background:${color}"></div></div>
<span style="font-size:0.7rem;width:22px;text-align:right">${val}</span>
</div>`;
}
function renderPlayerCard(item) {
const c = item.card;
const ovr = item.effective_overall ?? c.overall;
const pos = item.effective_position ?? c.position;
const trainBonus = item.training_bonus > 0 ? `+${item.training_bonus}` : '';
const chemStyle = item.chemistry_style && item.chemistry_style !== 'basic' ? item.chemistry_style : '';
const isLoan = item.is_loan ? `<span class="upgrade-pill loan-pill">LOAN${item.loan_matches_remaining != null ? ' ' + item.loan_matches_remaining : ''}</span>` : '';
const pills = [
chemStyle ? `<span class="upgrade-pill chem-pill">${chemStyle}</span>` : '',
trainBonus ? `<span class="upgrade-pill train-pill">OVR${trainBonus}</span>` : '',
isLoan,
].join('');
return `<div class="player-card" title="${c.club} · ${c.league} · ${c.nation}">
<span class="card-rarity ${rarityClass(c.rarity)}">${(c.rarity||'').toUpperCase()}</span>
<div class="card-ovr">${ovr}</div>
<div class="card-pos">${pos}</div>
<div class="card-name">${c.name}</div>
<div class="card-meta">${c.club}</div>
<div style="margin:6px 0 4px">
${statBar('PAC', c.pace)}${statBar('SHO', c.shooting)}${statBar('PAS', c.passing)}
${statBar('DRI', c.dribbling)}${statBar('DEF', c.defending)}${statBar('PHY', c.physical)}
</div>
${pills ? `<div class="card-upgrades">${pills}</div>` : ''}
</div>`;
}
// ── Club ─────────────────────────────────────────────────────────────────────
async function loadClub() {
try {
const club = await api('GET', '/club');
const profile = await api('GET', '/profile').catch(() => ({}));
const name = club.name || '—';
const mgr = club.manager_name || 'Player Manager';
el('hdr-club').textContent = name;
el('hdr-manager').textContent = mgr;
el('hdr-coins').textContent = (club.coins ?? 0).toLocaleString() + ' coins';
el('edit-name').value = name;
el('edit-manager').value = mgr;
el('club-info').innerHTML = `
<div class="info-row"><span class="lbl">Club name</span><strong>${name}</strong></div>
<div class="info-row"><span class="lbl">Manager</span>${mgr}</div>
<div class="info-row"><span class="lbl">Coins</span><strong style="color:#f5a623">${(club.coins ?? 0).toLocaleString()}</strong></div>
<div class="info-row"><span class="lbl">Level</span>${club.level ?? 1}</div>
<div class="info-row"><span class="lbl">Club ID</span><code style="font-size:0.75rem;color:#8b949e">${club.id}</code></div>
`;
} catch (e) { el('club-info').innerHTML = `<div class="error-msg">${e.message}</div>`; }
try {
const s = await api('GET', '/statistics');
el('stats-info').innerHTML = `
<div class="info-row"><span class="lbl">Matches</span>${s.matches_played ?? 0}</div>
<div class="info-row"><span class="lbl">Wins</span><span style="color:#3fb950">${s.wins ?? 0}</span></div>
<div class="info-row"><span class="lbl">Draws</span>${s.draws ?? 0}</div>
<div class="info-row"><span class="lbl">Losses</span><span style="color:#f85149">${s.losses ?? 0}</span></div>
<div class="info-row"><span class="lbl">Goals scored</span>${s.goals_scored ?? 0}</div>
<div class="info-row"><span class="lbl">Goals conceded</span>${s.goals_conceded ?? 0}</div>
<div class="info-row"><span class="lbl">Packs opened</span>${s.packs_opened ?? 0}</div>
<div class="info-row"><span class="lbl">Win streak</span>${s.current_win_streak ?? 0}</div>
`;
} catch (e) { el('stats-info').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
async function saveClub() {
const name = el('edit-name').value.trim();
const mgr = el('edit-manager').value.trim();
if (!name && !mgr) return;
try {
await api('PUT', '/club', { name: name || undefined, manager_name: mgr || undefined });
showToast('Club updated');
loadClub();
} catch (e) { showToast(e.message, true); }
}
// ── Collection ───────────────────────────────────────────────────────────────
async function loadCollection() {
try {
const data = await api('GET', '/collection');
allCards = data.collection || [];
filterCards();
} catch (e) { el('collection-grid').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
function filterCards() {
const name = el('f-name').value.toLowerCase();
const pos = el('f-pos').value;
const rarity = el('f-rarity').value;
const club = el('f-club').value.toLowerCase();
const nation = el('f-nation').value.toLowerCase();
const sort = el('f-sort').value;
let cards = allCards.filter(item => {
const c = item.card;
if (!c) return false;
if (name && !c.name.toLowerCase().includes(name)) return false;
if (pos && (item.effective_position || c.position) !== pos) return false;
if (rarity && (c.rarity || '').toLowerCase() !== rarity) return false;
if (club && !c.club.toLowerCase().includes(club)) return false;
if (nation && !c.nation.toLowerCase().includes(nation)) return false;
return true;
});
if (sort === 'name') cards.sort((a, b) => a.card.name.localeCompare(b.card.name));
else if (sort === 'pos') cards.sort((a, b) => a.card.position.localeCompare(b.card.position));
else cards.sort((a, b) => (b.effective_overall ?? b.card.overall) - (a.effective_overall ?? a.card.overall));
el('collection-count').textContent = `Showing ${cards.length} of ${allCards.length} cards`;
el('collection-grid').innerHTML = cards.length
? cards.map(renderPlayerCard).join('')
: '<span class="empty">No cards match the current filters.</span>';
}
// ── Packs ────────────────────────────────────────────────────────────────────
async function loadPacks() {
try {
const data = await api('GET', '/packs');
const packs = data.packs || [];
if (!packs.length) {
el('packs-grid').innerHTML = '<span class="empty">No unopened packs.</span>';
} else {
el('packs-grid').innerHTML = packs.map(p => `
<div class="pack-item">
<h3>${p.name || p.definition_id}</h3>
<div class="pack-id">${p.pack_id}</div>
<button class="btn btn-primary btn-sm" onclick="openPack('${p.pack_id}', this)">Open Pack</button>
</div>
`).join('');
}
} catch (e) { el('packs-grid').innerHTML = `<div class="error-msg">${e.message}</div>`; }
try {
const hist = await api('GET', '/packs/history');
const history = hist.history || [];
if (!history.length) {
el('pack-history').innerHTML = '<span class="empty">No packs opened yet.</span>';
} else {
el('pack-history').innerHTML = history.slice(0, 10).map(h => `
<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:12px;margin-bottom:8px;">
<div style="display:flex;justify-content:space-between;font-size:0.85rem">
<strong>${h.name || h.definition_id}</strong>
<span style="color:#8b949e;font-size:0.75rem">${h.opened_at ? new Date(h.opened_at).toLocaleString() : '—'}</span>
</div>
<div style="font-size:0.75rem;color:#8b949e;margin-top:4px">${(h.cards||[]).length} cards received</div>
</div>
`).join('');
}
} catch (e) { el('pack-history').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
async function openPack(packId, btn) {
btn.disabled = true;
btn.textContent = 'Opening…';
try {
const result = await api('POST', `/packs/open/${packId}`);
const cards = result.cards || [];
el('modal-cards').innerHTML = cards.map(c => `
<div class="player-card" style="font-size:0.8rem">
<span class="card-rarity ${rarityClass(c.rarity)}">${(c.rarity||'').toUpperCase()}</span>
<div class="card-ovr" style="font-size:1.2rem">${c.overall}</div>
<div class="card-pos" style="font-size:0.65rem">${c.position}</div>
<div class="card-name">${c.name}</div>
<div class="card-meta">${c.club}</div>
</div>
`).join('');
el('pack-modal').classList.add('open');
showToast(`Opened ${cards.length} cards!`);
loadPacks();
// Refresh header coins
api('GET', '/club').then(c => {
el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins';
}).catch(() => {});
} catch (e) {
showToast(e.message, true);
btn.disabled = false;
btn.textContent = 'Open Pack';
}
}
function closeModal() {
el('pack-modal').classList.remove('open');
}
// ── Objectives ───────────────────────────────────────────────────────────────
async function loadObjectives() {
try {
const data = await api('GET', '/objectives');
const objs = data.objectives || [];
if (!objs.length) {
el('objectives-list').innerHTML = '<span class="empty">No objectives found.</span>';
return;
}
el('objectives-list').innerHTML = objs.map(obj => {
const pct = obj.target > 0 ? Math.min(100, Math.round((obj.current / obj.target) * 100)) : 0;
const cls = obj.claimed ? 'claimed' : obj.completed ? 'completed' : '';
const reward = [
obj.reward_coins ? `${obj.reward_coins.toLocaleString()} coins` : '',
obj.reward_pack_id ? `+ ${obj.reward_pack_id}` : '',
].filter(Boolean).join(' ');
const claimBtn = obj.completed && !obj.claimed
? `<button class="btn btn-primary btn-sm" onclick="claimObjective('${obj.id}', this)">Claim</button>` : '';
const statusLabel = obj.claimed ? '✓ Claimed' : obj.completed ? '✓ Complete' : `${obj.current}/${obj.target}`;
return `<div class="obj-item ${cls}">
<div class="obj-header">
<span class="obj-title">${obj.title}</span>
${reward ? `<span class="obj-reward">${reward}</span>` : ''}
${claimBtn}
</div>
<div class="progress-track"><div class="progress-fill${obj.completed ? ' done' : ''}" style="width:${pct}%"></div></div>
<div class="obj-meta"><span>${obj.description || ''}</span><span>${statusLabel}</span></div>
</div>`;
}).join('');
} catch (e) { el('objectives-list').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
async function claimObjective(id, btn) {
btn.disabled = true;
try {
const r = await api('POST', `/objectives/${id}/claim`);
showToast(`Claimed! +${(r.coins_awarded || 0).toLocaleString()} coins`);
loadObjectives();
api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {});
} catch (e) { showToast(e.message, true); btn.disabled = false; }
}
// ── Division ─────────────────────────────────────────────────────────────────
async function loadDivision() {
try {
const d = await api('GET', '/division');
const rec = d.record || {};
const ptsMax = 30; // 10 matches × 3 pts
const ptsPct = Math.min(100, Math.round((d.season_points / ptsMax) * 100));
const promoPct = d.pts_for_promotion > 0
? Math.min(100, Math.round((d.season_points / d.pts_for_promotion) * 100)) : 100;
el('division-card').innerHTML = `
<div class="division-num">${d.division}</div>
<div class="division-label">Division · Season ${d.season_number}</div>
<div class="record-row">
<div class="record-cell w"><div class="val">${rec.wins ?? 0}</div><div class="lbl">W</div></div>
<div class="record-cell d"><div class="val">${rec.draws ?? 0}</div><div class="lbl">D</div></div>
<div class="record-cell l"><div class="val">${rec.losses ?? 0}</div><div class="lbl">L</div></div>
</div>
`;
el('season-progress').innerHTML = `
<div class="info-row"><span class="lbl">Points</span><strong>${d.season_points}</strong></div>
<div class="info-row"><span class="lbl">For promotion</span>${d.pts_for_promotion} pts needed</div>
<div class="progress-track" style="margin:8px 0 16px">
<div class="progress-fill${promoPct >= 100 ? ' done' : ''}" style="width:${promoPct}%"></div>
</div>
<div class="info-row"><span class="lbl">Matches played</span>${d.matches_played} / 10</div>
<div class="info-row"><span class="lbl">Remaining</span>${d.matches_remaining}</div>
<div class="info-row"><span class="lbl">Started</span>${d.started_at ? new Date(d.started_at).toLocaleDateString() : '—'}</div>
`;
} catch (e) {
el('division-card').innerHTML = `<div class="error-msg">${e.message}</div>`;
el('season-progress').innerHTML = '';
}
}
async function claimRivalsReward() {
try {
const r = await api('POST', '/rivals/claim-weekly');
showToast(`Week ${r.week_number} reward: +${(r.coins_awarded || 0).toLocaleString()} coins${r.pack_granted ? ' + pack' : ''}`);
loadDivision();
api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {});
} catch (e) { showToast(e.message, true); }
}
// ── FUT Champions ─────────────────────────────────────────────────────────────
let currentChampsSession = null;
async function loadChamps() {
try {
const data = await api('GET', '/fut-champs');
currentChampsSession = data.session;
renderChampsSession(data.session, data.max_matches);
} catch (e) { el('champs-current').innerHTML = `<div class="error-msg">${e.message}</div>`; }
try {
const hist = await api('GET', '/fut-champs/history');
const sessions = hist.history || [];
if (!sessions.length) {
el('champs-history').innerHTML = '<span class="empty">No past sessions.</span>';
} else {
el('champs-history').innerHTML = sessions.map(s => {
const tierClass = s.reward_tier ? 'tier-' + s.reward_tier.split(' ')[0] : '';
return `<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:12px;margin-bottom:8px;">
<div style="display:flex;justify-content:space-between;align-items:center">
<strong>Week ${s.week_number}</strong>
${s.reward_tier ? `<span class="tier-badge ${tierClass}">${s.reward_tier}</span>` : ''}
</div>
<div style="font-size:0.8rem;color:#8b949e;margin-top:4px">
${s.wins}W ${s.draws}D ${s.losses}L · ${s.matches_played}/30 matches
${s.reward_coins ? ` · ${s.reward_coins.toLocaleString()} coins` : ''}
${s.reward_claimed ? ' · <span style="color:#3fb950">✓ claimed</span>' : ''}
</div>
</div>`;
}).join('');
}
} catch (e) { el('champs-history').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
function renderChampsSession(session, maxMatches) {
if (!session) {
el('champs-current').innerHTML = `
<div class="champs-card">
<p style="color:#8b949e;margin-bottom:16px">No active FUT Champions session.</p>
<button class="btn btn-primary" onclick="startChampsSession()">Start New Week</button>
</div>`;
return;
}
const pct = Math.round((session.matches_played / (maxMatches || 30)) * 100);
const tierClass = session.reward_tier ? 'tier-' + session.reward_tier.split(' ')[0] : '';
const isComplete = session.status === 'completed';
el('champs-current').innerHTML = `
<div class="champs-card">
<div style="display:flex;justify-content:space-between;align-items:center">
<strong>Week ${session.week_number}</strong>
<span style="font-size:0.8rem;color:${isComplete ? '#3fb950' : '#8b949e'}">${isComplete ? 'Completed' : 'Active'}</span>
</div>
${session.reward_tier ? `<div class="tier-badge ${tierClass}">${session.reward_tier}</div>` : ''}
<div class="record-row" style="margin:12px 0">
<div class="record-cell w"><div class="val">${session.wins}</div><div class="lbl">W</div></div>
<div class="record-cell d"><div class="val">${session.draws}</div><div class="lbl">D</div></div>
<div class="record-cell l"><div class="val">${session.losses}</div><div class="lbl">L</div></div>
</div>
<div style="font-size:0.8rem;color:#8b949e;margin-bottom:6px">${session.matches_played} / ${maxMatches || 30} matches played</div>
<div class="progress-track" style="margin-bottom:16px">
<div class="progress-fill${isComplete ? ' done' : ''}" style="width:${pct}%"></div>
</div>
${!isComplete ? `
<div style="display:flex;gap:8px;align-items:center;margin-bottom:12px">
<span style="font-size:0.8rem;color:#8b949e">Simulate result:</span>
<button class="btn btn-secondary btn-sm" onclick="simulateChampsMatch(3,0)">Win (3-0)</button>
<button class="btn btn-secondary btn-sm" onclick="simulateChampsMatch(1,1)">Draw (1-1)</button>
<button class="btn btn-secondary btn-sm" onclick="simulateChampsMatch(0,2)">Loss (0-2)</button>
</div>` : ''}
${isComplete && !session.reward_claimed ? `
<div style="background:#0d2818;border-radius:6px;padding:12px;margin-bottom:12px">
<div style="font-size:0.85rem"><strong>Reward ready!</strong> ${(session.reward_coins || 0).toLocaleString()} coins${session.reward_pack_id ? ' + ' + session.reward_pack_id : ''}</div>
</div>
<button class="btn btn-primary" onclick="claimChampsReward('${session.id}')">Claim Reward</button>` : ''}
${session.reward_claimed ? '<div style="color:#3fb950;font-size:0.85rem">✓ Reward claimed</div>' : ''}
</div>`;
}
async function startChampsSession() {
try {
const data = await api('POST', '/fut-champs/start');
currentChampsSession = data.session;
showToast('FUT Champions week started!');
loadChamps();
} catch (e) { showToast(e.message, true); }
}
async function simulateChampsMatch(gf, ga) {
if (!currentChampsSession) return;
try {
const r = await api('POST', `/fut-champs/${currentChampsSession.id}/result`, { goals_for: gf, goals_against: ga });
currentChampsSession = r.session;
showToast(`Match recorded: ${r.outcome}`);
loadChamps();
} catch (e) { showToast(e.message, true); }
}
async function claimChampsReward(sessionId) {
try {
const r = await api('POST', `/fut-champs/${sessionId}/claim`);
showToast(`Claimed ${r.tier}: +${(r.coins_awarded || 0).toLocaleString()} coins!`);
currentChampsSession = null;
api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {});
loadChamps();
} catch (e) { showToast(e.message, true); }
}
// ── Matches ───────────────────────────────────────────────────────────────────
let currentOpponent = null;
async function loadMatches() {
await loadMatchHistory();
}
async function generateOpponent() {
const diff = el('match-difficulty').value;
try {
const data = await api('GET', `/matches/opponent?difficulty=${diff}`);
currentOpponent = data;
const rating = data.squad_rating ?? '—';
const name = data.opponent_name ?? 'AI Opponent';
const formation = data.formation ?? '—';
el('opponent-display').innerHTML = `
<div class="opponent-card">
<div style="font-size:0.8rem;color:#8b949e;margin-bottom:2px">${diff.replace('_', ' ').toUpperCase()}</div>
<div style="font-weight:600">${name}</div>
<div style="display:flex;gap:16px;align-items:baseline;margin-top:4px">
<span class="opponent-rating">${rating}</span>
<span class="formation-badge">${formation}</span>
</div>
</div>`;
} catch (e) { showToast(e.message, true); }
}
function quickScore(gf, ga) {
el('goals-for').value = gf;
el('goals-against').value = ga;
}
async function submitMatch() {
const gf = parseInt(el('goals-for').value, 10) || 0;
const ga = parseInt(el('goals-against').value, 10) || 0;
const diff = el('match-difficulty').value;
const opponentName = currentOpponent?.opponent_name ?? `${diff.replace('_',' ')} Bot`;
try {
const r = await api('POST', '/matches/result', {
squad_id: 'dashboard',
opponent_name: opponentName,
goals_for: gf,
goals_against: ga,
mode: 'squad_battles',
});
const outcome = gf > ga ? 'Win' : gf === ga ? 'Draw' : 'Loss';
const outcomeColor = gf > ga ? '#3fb950' : gf === ga ? '#8b949e' : '#f85149';
const levelUps = r.level_ups || [];
const levelUpHtml = levelUps.map(ev =>
`<div style="color:#f5a623;font-size:0.78rem">⬆ Level ${ev.new_level}! +${(ev.coins_granted||0).toLocaleString()} coins${ev.pack_granted ? ' + ' + ev.pack_granted : ''}</div>`
).join('');
el('match-result-display').innerHTML = `
<div style="background:#0d1117;border-radius:6px;padding:12px;border-left:3px solid ${outcomeColor}">
<div style="font-weight:700;color:${outcomeColor}">${outcome} ${gf}${ga}</div>
<div style="font-size:0.8rem;margin-top:4px">
+${(r.coins_awarded || 0).toLocaleString()} coins · +${r.xp_awarded || 0} XP
${(r.objectives_updated || []).length ? `<br>Objectives: ${r.objectives_updated.join(', ')}` : ''}
${r.season_end ? `<br>Season ended: Division ${r.season_end.new_division ?? '—'}` : ''}
</div>
${levelUpHtml}
</div>`;
if (levelUps.length) {
showToast(`Level Up! You are now level ${levelUps[levelUps.length - 1].new_level}`);
}
api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {});
await loadMatchHistory();
} catch (e) { showToast(e.message, true); }
}
async function loadMatchHistory() {
try {
const data = await api('GET', '/matches?limit=20');
const matches = data.matches || [];
if (!matches.length) {
el('match-history').innerHTML = '<span class="empty">No matches played yet.</span>';
return;
}
el('match-history').innerHTML = matches.map(m => {
const cls = m.outcome === 'win' ? 'win' : m.outcome === 'draw' ? 'draw' : 'loss';
const lbl = m.outcome === 'win' ? 'W' : m.outcome === 'draw' ? 'D' : 'L';
return `<div class="match-history-row">
<span class="outcome-badge outcome-${cls}">${lbl}</span>
<span style="flex:1">${m.opponent_name}</span>
<span style="font-weight:600">${m.goals_for}${m.goals_against}</span>
<span style="color:#f5a623;font-size:0.75rem">+${(m.coins_awarded||0).toLocaleString()}</span>
<span style="color:#8b949e;font-size:0.72rem">${m.mode || 'squad_battles'}</span>
</div>`;
}).join('');
} catch (e) { el('match-history').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
// ── Pack Store ────────────────────────────────────────────────────────────────
async function loadStore() {
try {
const data = await api('GET', '/packs/store');
const packs = data.packs || [];
if (!packs.length) {
el('store-grid').innerHTML = '<span class="empty">No packs available.</span>';
return;
}
el('store-grid').innerHTML = packs.map(p => `
<div class="pack-item">
<h3>${p.name}</h3>
<div class="pack-id" style="word-break:normal">${p.description}</div>
<div style="margin:8px 0">
<span class="market-price">${(p.cost_coins || 0).toLocaleString()} coins</span>
<span style="font-size:0.75rem;color:#8b949e;margin-left:6px">${p.total_cards} cards</span>
</div>
<button class="btn btn-primary btn-sm" onclick="buyStorePack('${p.id}', this)">Buy</button>
</div>`).join('');
} catch (e) { el('store-grid').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
async function buyStorePack(defId, btn) {
btn.disabled = true; btn.textContent = 'Buying…';
try {
await api('POST', '/packs/buy', { pack_definition_id: defId });
showToast('Pack purchased! Open it in the Packs tab.');
api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {});
btn.disabled = false; btn.textContent = 'Buy';
} catch (e) { showToast(e.message, true); btn.disabled = false; btn.textContent = 'Buy'; }
}
// ── SBC ───────────────────────────────────────────────────────────────────────
// Per-SBC selected owned card IDs: { sbcId: [ownedCardId, ...] }
const sbcSelections = {};
async function loadSbc() {
if (!allCards.length) {
try { const d = await api('GET', '/collection'); allCards = d.collection || []; } catch (_) {}
}
try {
const data = await api('GET', '/sbc');
const sbcs = data.sbcs || [];
if (!sbcs.length) {
el('sbc-list').innerHTML = '<span class="empty">No SBC challenges defined.</span>';
return;
}
el('sbc-list').innerHTML = sbcs.map(s => renderSbcCard(s)).join('');
} catch (e) { el('sbc-list').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
function renderSbcCard(s) {
const r = s.requirements || {};
const reward = s.reward || {};
const reqs = [
r.squad_size ? `${r.squad_size} players` : '',
r.min_overall ? `Min OVR ${r.min_overall}` : '',
r.max_overall ? `Max OVR ${r.max_overall}` : '',
r.min_chemistry ? `Min Chem ${r.min_chemistry}` : '',
...(r.required_nations || []).map(n => `Nation: ${n}`),
...(r.required_leagues || []).map(l => `League: ${l}`),
...(r.required_clubs || []).map(c => `Club: ${c}`),
r.min_players_from_same_league ? `${r.min_players_from_same_league}+ same league` : '',
r.min_players_from_same_nation ? `${r.min_players_from_same_nation}+ same nation` : '',
r.min_players_from_same_club ? `${r.min_players_from_same_club}+ same club` : '',
].filter(Boolean);
const rewardStr = [
reward.coins ? `${reward.coins.toLocaleString()} coins` : '',
reward.xp ? `${reward.xp} XP` : '',
reward.pack_id ? reward.pack_id.replace(/_/g,' ') : '',
].filter(Boolean).join(' + ');
const sel = sbcSelections[s.id] || [];
const needed = r.squad_size || 11;
const cardOpts = allCards
.filter(i => !i.is_loan)
.sort((a, b) => (b.effective_overall ?? b.card.overall) - (a.effective_overall ?? a.card.overall))
.map(i => {
const c = i.card;
const ovr = i.effective_overall ?? c.overall;
return `<option value="${i.owned_card_id}">${ovr} ${c.name} (${c.position}, ${c.club})</option>`;
}).join('');
const selPills = sel.map(id => {
const item = allCards.find(i => i.owned_card_id === id);
const label = item ? `${item.card.name}` : id;
return `<span class="sbc-sel-pill" onclick="removeSbcCard('${s.id}','${id}')" title="Click to remove">${label} ×</span>`;
}).join('');
return `<div class="sbc-card" id="sbc-${s.id}">
<div class="sbc-name">${s.name}${s.repeatable ? ' <span style="font-size:0.7rem;color:#8b949e">(repeatable)</span>' : ''}</div>
<div class="sbc-desc">${s.description}</div>
<div class="sbc-reqs">${reqs.map(r => `<span class="req-pill">${r}</span>`).join('')}</div>
<div class="sbc-reward">Reward: ${rewardStr || '—'}</div>
<div class="sbc-picker">
<div style="font-size:0.78rem;color:#8b949e;margin-bottom:4px">
Selected: ${sel.length}/${needed} cards
</div>
<div class="sbc-selected" id="sbc-sel-${s.id}">${selPills}</div>
${sel.length < needed ? `
<select onchange="addSbcCard('${s.id}', this)">
<option value="">+ Add card…</option>
${cardOpts}
</select>` : ''}
${sel.length >= needed ? `
<button class="btn btn-primary btn-sm" onclick="submitSbc('${s.id}')">Submit SBC</button>` : ''}
</div>
</div>`;
}
function addSbcCard(sbcId, selectEl) {
const val = selectEl.value;
if (!val) return;
if (!sbcSelections[sbcId]) sbcSelections[sbcId] = [];
if (!sbcSelections[sbcId].includes(val)) sbcSelections[sbcId].push(val);
selectEl.value = '';
refreshSbcCard(sbcId);
}
function removeSbcCard(sbcId, ownedCardId) {
if (sbcSelections[sbcId]) {
sbcSelections[sbcId] = sbcSelections[sbcId].filter(id => id !== ownedCardId);
}
refreshSbcCard(sbcId);
}
async function refreshSbcCard(sbcId) {
try {
const data = await api('GET', '/sbc');
const sbc = (data.sbcs || []).find(s => s.id === sbcId);
if (sbc) {
const el_card = document.getElementById(`sbc-${sbcId}`);
if (el_card) el_card.outerHTML = renderSbcCard(sbc);
}
} catch (_) {}
}
async function submitSbc(sbcId) {
const owned = sbcSelections[sbcId] || [];
if (!owned.length) { showToast('Select cards first', true); return; }
try {
const r = await api('POST', '/sbc/submit', { sbc_id: sbcId, owned_card_ids: owned });
if (r.passed) {
delete sbcSelections[sbcId];
const reward = r.reward || {};
showToast(`SBC complete! ${reward.coins ? reward.coins.toLocaleString() + ' coins' : ''} ${reward.pack_id ? '+ ' + reward.pack_id : ''}`);
allCards = [];
api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {});
await loadSbc();
} else {
showToast('Failed: ' + (r.failures || []).join('; '), true);
}
} catch (e) { showToast(e.message, true); }
}
// ── Statistics ────────────────────────────────────────────────────────────────
async function loadStatistics() {
try {
const s = await api('GET', '/statistics');
const pg = s.position_goals || {};
el('career-stats').innerHTML = `
<div class="info-row"><span class="lbl">Matches</span><strong>${s.matches_played ?? 0}</strong></div>
<div class="info-row"><span class="lbl">Wins</span><span style="color:#3fb950">${s.wins ?? 0}</span></div>
<div class="info-row"><span class="lbl">Draws</span>${s.draws ?? 0}</div>
<div class="info-row"><span class="lbl">Losses</span><span style="color:#f85149">${s.losses ?? 0}</span></div>
<div class="info-row"><span class="lbl">Win rate</span>${s.matches_played > 0 ? Math.round(((s.wins??0)/s.matches_played)*100) : 0}%</div>
<div class="info-row"><span class="lbl">Goals scored</span>${s.goals_scored ?? 0}</div>
<div class="info-row"><span class="lbl">Goals conceded</span>${s.goals_conceded ?? 0}</div>
<div class="info-row"><span class="lbl">Avg goals/game</span>${s.matches_played > 0 ? ((s.goals_scored??0)/s.matches_played).toFixed(1) : '0.0'}</div>
<div class="info-row"><span class="lbl">Win streak</span>${s.current_win_streak ?? 0}</div>
<div class="info-row"><span class="lbl">Best streak</span>${s.best_win_streak ?? 0}</div>
<div class="info-row"><span class="lbl">Packs opened</span>${s.packs_opened ?? 0}</div>
<div class="info-row"><span class="lbl">SBCs done</span>${s.sbcs_completed ?? 0}</div>
<div class="info-row"><span class="lbl">Coins earned</span>${(s.total_coins_earned ?? 0).toLocaleString()}</div>
`;
const posEntries = Object.entries(pg).sort((a, b) => b[1] - a[1]);
el('position-goals').innerHTML = posEntries.length
? posEntries.map(([pos, goals]) => {
const max = posEntries[0][1] || 1;
const pct = Math.round((goals / max) * 100);
return `<div class="stat-bar-row" style="font-size:0.8rem">
<span class="stat-bar-label" style="width:36px">${pos}</span>
<div class="stat-bar-track"><div class="stat-bar-fill" style="width:${pct}%"></div></div>
<span style="font-size:0.75rem;width:22px;text-align:right">${goals}</span>
</div>`;
}).join('')
: '<span class="empty">No goal data yet.</span>';
} catch (e) { el('career-stats').innerHTML = `<div class="error-msg">${e.message}</div>`; }
try {
const h = await api('GET', '/statistics/history?limit=20');
const matches = h.matches || [];
if (!matches.length) {
el('stats-history').innerHTML = '<span class="empty">No match history yet.</span>';
return;
}
const summ = h.summary || {};
el('stats-history').innerHTML = `
<div style="display:flex;gap:24px;font-size:0.85rem;margin-bottom:16px;flex-wrap:wrap">
<span>Last ${matches.length} matches</span>
<span style="color:#3fb950">${summ.wins ?? 0}W</span>
<span>${summ.draws ?? 0}D</span>
<span style="color:#f85149">${summ.losses ?? 0}L</span>
<span>Avg goals: ${summ.avg_goals_per_game ?? 0}</span>
<span>Win rate: ${((summ.win_rate ?? 0)*100).toFixed(0)}%</span>
</div>` +
matches.map(m => {
const cls = m.outcome === 'win' ? 'win' : m.outcome === 'draw' ? 'draw' : 'loss';
const lbl = { win: 'W', draw: 'D', loss: 'L' }[m.outcome] || '?';
return `<div class="match-history-row">
<span class="outcome-badge outcome-${cls}">${lbl}</span>
<span style="flex:1">${m.opponent_name}</span>
<span style="font-weight:600">${m.goals_for}${m.goals_against}</span>
<span style="color:#f5a623;font-size:0.75rem">+${(m.coins_awarded||0).toLocaleString()}</span>
<span style="color:#8b949e;font-size:0.72rem">${new Date(m.played_at).toLocaleDateString()}</span>
</div>`;
}).join('');
} catch (e) { el('stats-history').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
// ── Card Catalog ──────────────────────────────────────────────────────────────
let catalogCards = [];
let catalogOwned = new Set();
let catalogLimit = 80;
async function loadCatalog() {
el('catalog-grid').innerHTML = '<span class="loading">Loading…</span>';
try {
const [cardsData, collData] = await Promise.all([
api('GET', '/cards'),
api('GET', '/collection').catch(() => ({ collection: [] })),
]);
catalogCards = cardsData.cards || [];
catalogOwned = new Set((collData.collection || []).map(i => i.card?.id).filter(Boolean));
catalogLimit = 80;
filterCatalog();
} catch (e) { el('catalog-grid').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
function filterCatalog() {
const name = (el('cat-name')?.value || '').toLowerCase();
const pos = el('cat-pos')?.value || '';
const rar = el('cat-rarity')?.value || '';
const nat = (el('cat-nation')?.value || '').toLowerCase();
const lea = (el('cat-league')?.value || '').toLowerCase();
const club = (el('cat-club')?.value || '').toLowerCase();
const minO = parseInt(el('cat-min-ovr')?.value || '0', 10) || 0;
const maxO = parseInt(el('cat-max-ovr')?.value || '99', 10) || 99;
const filtered = catalogCards.filter(c => {
if (name && !c.name.toLowerCase().includes(name)) return false;
if (pos && c.position !== pos) return false;
if (rar && (c.rarity||'').toLowerCase() !== rar) return false;
if (nat && !c.nation.toLowerCase().includes(nat)) return false;
if (lea && !c.league.toLowerCase().includes(lea)) return false;
if (club && !c.club.toLowerCase().includes(club)) return false;
if (c.overall < minO || c.overall > maxO) return false;
return true;
});
el('catalog-count').textContent = `${filtered.length} card${filtered.length !== 1 ? 's' : ''} in catalog`;
const shown = filtered.slice(0, catalogLimit);
el('catalog-grid').innerHTML = shown.map(c => {
const owned = catalogOwned.has(c.id);
return `<div class="player-card" style="position:relative" title="${c.club} · ${c.league} · ${c.nation}">
${owned ? '<div class="owned-overlay"></div><span class="owned-tag">Owned</span>' : ''}
<span class="card-rarity ${rarityClass(c.rarity)}">${(c.rarity||'').toUpperCase()}</span>
<div class="card-ovr">${c.overall}</div>
<div class="card-pos">${c.position}</div>
<div class="card-name">${c.name}</div>
<div class="card-meta">${c.club}</div>
<div style="margin:6px 0 4px">
${statBar('PAC',c.pace)}${statBar('SHO',c.shooting)}${statBar('PAS',c.passing)}
${statBar('DRI',c.dribbling)}${statBar('DEF',c.defending)}${statBar('PHY',c.physical)}
</div>
</div>`;
}).join('');
const moreBtn = el('catalog-more-btn');
if (filtered.length > catalogLimit) {
moreBtn.style.display = '';
moreBtn.textContent = `Load more (${filtered.length - catalogLimit} remaining)`;
} else {
moreBtn.style.display = 'none';
}
}
function catalogLoadMore() {
catalogLimit += 80;
filterCatalog();
}
// ── Settings & Profile ────────────────────────────────────────────────────────
async function loadSettings() {
try {
const [profile, settings] = await Promise.all([
api('GET', '/profile'),
api('GET', '/settings'),
]);
const xpForNext = profile.xp_for_next_level || 1000;
const xpEarned = xpForNext - (profile.xp_to_next_level || 0);
const xpPct = Math.min(100, Math.round((xpEarned / xpForNext) * 100));
el('profile-card').innerHTML = `
<div class="info-row"><span class="lbl">Username</span><strong>${profile.username || '—'}</strong></div>
<div class="info-row"><span class="lbl">Level</span><strong>${profile.level ?? 1}</strong></div>
<div class="info-row"><span class="lbl">XP</span>${(profile.xp || 0).toLocaleString()}
<span style="color:#8b949e;font-size:0.75rem;margin-left:4px">${(profile.xp_to_next_level || 0).toLocaleString()} to next level</span>
</div>
<div class="xp-bar"><div class="xp-fill" style="width:${xpPct}%"></div></div>
<div style="font-size:0.75rem;color:#8b949e">Profile ID: <code>${profile.id}</code></div>
`;
const diffSel = el('set-difficulty');
const frmSel = el('set-formation');
if (diffSel) diffSel.value = settings.difficulty || 'beginner';
if (frmSel) frmSel.value = settings.preferred_formation || '4-4-2';
} catch (e) {
el('profile-card').innerHTML = `<div class="error-msg">${e.message}</div>`;
}
}
async function saveSettings() {
const difficulty = el('set-difficulty').value;
const preferred_formation = el('set-formation').value;
try {
await api('PUT', '/settings', { difficulty, preferred_formation });
const saved = el('settings-saved');
saved.style.display = '';
setTimeout(() => { saved.style.display = 'none'; }, 2000);
} catch (e) { showToast(e.message, true); }
}
// ── Squad ─────────────────────────────────────────────────────────────────────
async function loadSquad() {
try {
const data = await api('GET', '/squad');
const squad = data.squad;
const players = data.players || [];
const chem = data.chemistry || {};
el('squad-header').innerHTML = `
<div style="display:flex;gap:10px;align-items:center;margin-bottom:12px">
<strong>${squad?.name || 'My Squad'}</strong>
<span class="formation-badge">${squad?.formation || '—'}</span>
</div>`;
if (!players.length) {
el('squad-grid').innerHTML = '<span class="empty">No squad saved yet. Use /squad POST from the game or API.</span>';
el('squad-chemistry').innerHTML = '';
return;
}
const playerChems = chem.players || {};
el('squad-grid').innerHTML = players.map(p => {
const card = p.card;
const pChem = playerChems[p.owned_card_id] ?? 0;
const dots = Array.from({length: 10}, (_, i) =>
`<span class="chem-dot ${i < pChem ? (pChem >= 7 ? 'full' : 'partial') : 'none'}"></span>`
).join('');
return `<div class="squad-slot">
<div class="slot-pos">${p.position_label || 'Player'}${p.is_on_bench ? ' (sub)' : ''}</div>
${card ? `
<div class="card-ovr" style="font-size:1.2rem">${card.overall}</div>
<div style="font-size:0.8rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${card.name}</div>
<div style="font-size:0.7rem;color:#8b949e">${card.position} · ${card.club}</div>
<div style="margin-top:4px;font-size:0.65rem;color:#8b949e">Chem: ${pChem}/10</div>
<div>${dots}</div>
` : '<span style="color:#8b949e;font-size:0.8rem">Empty</span>'}
</div>`;
}).join('');
const total = chem.total ?? 0;
const pct = total;
const chemColor = total >= 80 ? '#3fb950' : total >= 50 ? '#d29922' : '#f85149';
el('squad-chemistry').innerHTML = `
<div style="font-size:2.5rem;font-weight:800;color:${chemColor}">${total}<span style="font-size:1rem;color:#8b949e">/100</span></div>
<div class="chem-bar" style="margin:8px 0 16px"><div class="chem-bar-fill" style="width:${pct}%;background:${chemColor}"></div></div>
<p style="font-size:0.8rem;color:#8b949e">
Chemistry is earned through same-club, same-league, and same-nation links between adjacent players.
Each player earns up to 10 chem points; total is capped at 100.
</p>`;
} catch (e) {
el('squad-grid').innerHTML = `<div class="error-msg">${e.message}</div>`;
el('squad-chemistry').innerHTML = '';
}
await loadSquadList();
}
async function loadSquadList() {
try {
const data = await api('GET', '/squads');
const squads = data.squads || [];
if (!squads.length) {
el('squad-list').innerHTML = '<span class="empty">No squads saved yet.</span>';
return;
}
el('squad-list').innerHTML = squads.map(s => `
<div style="background:#21262d;border-radius:6px;padding:8px 12px;margin-bottom:6px;
display:flex;align-items:center;gap:10px;font-size:0.85rem">
<span style="flex:1;font-weight:600">${s.name}</span>
<span class="formation-badge">${s.formation}</span>
<button class="btn btn-danger btn-sm" onclick="deleteSquad('${s.id}', this)">Delete</button>
</div>`).join('');
} catch (e) { el('squad-list').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
async function deleteSquad(squadId, btn) {
if (!confirm('Delete this squad?')) return;
btn.disabled = true;
try {
await api('DELETE', `/squads/${squadId}`);
showToast('Squad deleted');
loadSquad();
} catch (e) { showToast(e.message, true); btn.disabled = false; }
}
// ── Squad Builder ────────────────────────────────────────────────────────────
// Formation → position labels for each of the 11 starter slots
const FORMATION_POSITIONS = {
'4-4-2': ['GK','RB','CB','CB','LB','RM','CM','CM','LM','ST','ST'],
'4-3-3': ['GK','RB','CB','CB','LB','CM','CM','CM','RW','ST','LW'],
'4-2-3-1': ['GK','RB','CB','CB','LB','CDM','CDM','CAM','RW','LW','ST'],
'4-1-2-1-2':['GK','RB','CB','CB','LB','CDM','CM','CM','CAM','ST','ST'],
'3-5-2': ['GK','CB','CB','CB','RM','CDM','CM','LM','CAM','ST','ST'],
'5-3-2': ['GK','RB','CB','CB','CB','LB','CM','CM','CM','ST','ST'],
};
const BENCH_LABELS = ['SUB 1','SUB 2','SUB 3','SUB 4','SUB 5','SUB 6','SUB 7'];
// Builder state: slot index → owned card id
const builderStarters = {};
const builderBench = {};
let pickerTargetSlot = null; // { type:'starter'|'bench', index }
function squadMode(mode) {
el('squad-view-mode').style.display = mode === 'view' ? '' : 'none';
el('squad-build-mode').style.display = mode === 'build' ? '' : 'none';
el('squad-view-btn').style.borderColor = mode === 'view' ? '#f5a623' : '';
el('squad-view-btn').style.color = mode === 'view' ? '#f5a623' : '';
el('squad-build-btn').style.borderColor = mode === 'build' ? '#f5a623' : '';
el('squad-build-btn').style.color = mode === 'build' ? '#f5a623' : '';
if (mode === 'build') {
rebuildSlots();
if (!allCards.length) {
api('GET', '/collection').then(d => { allCards = d.collection || []; }).catch(() => {});
}
}
}
function rebuildSlots() {
const formation = el('builder-formation')?.value || '4-4-2';
const positions = FORMATION_POSITIONS[formation] || FORMATION_POSITIONS['4-4-2'];
el('builder-starters').innerHTML = positions.map((pos, i) => slotHtml('starter', i, pos)).join('');
el('builder-bench').innerHTML = BENCH_LABELS.map((lbl, i) => slotHtml('bench', i, lbl)).join('');
}
function slotHtml(type, index, label) {
const store = type === 'starter' ? builderStarters : builderBench;
const ownedId = store[index];
const item = ownedId ? allCards.find(c => c.owned_card_id === ownedId) : null;
const isGk = label === 'GK';
if (item) {
const c = item.card;
const ovr = item.effective_overall ?? c.overall;
return `<div class="builder-slot filled${isGk ? ' gk-slot' : ''}" onclick="openPicker('${type}',${index},'${label}')">
<div class="slot-label">${label}</div>
<span class="slot-clear" onclick="event.stopPropagation();clearSlot('${type}',${index})">✕</span>
<div style="font-size:0.75rem;font-weight:700;color:#f5a623">${ovr}</div>
<div style="font-size:0.78rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${c.name}</div>
<div style="font-size:0.65rem;color:#8b949e">${c.position}</div>
</div>`;
}
return `<div class="builder-slot${isGk ? ' gk-slot' : ''}" onclick="openPicker('${type}',${index},'${label}')">
<div class="slot-label">${label}</div>
<div style="color:#8b949e;font-size:0.8rem;margin-top:8px">+ Pick player</div>
</div>`;
}
function openPicker(type, index, label) {
pickerTargetSlot = { type, index };
el('picker-slot-label').textContent = label;
el('picker-search').value = '';
el('picker-overlay').style.display = '';
renderPicker();
}
function closePicker() {
el('picker-overlay').style.display = 'none';
pickerTargetSlot = null;
}
function renderPicker() {
const q = el('picker-search').value.toLowerCase();
const used = new Set([
...Object.values(builderStarters),
...Object.values(builderBench),
]);
const cards = allCards
.filter(i => !i.is_loan && !used.has(i.owned_card_id))
.filter(i => !q || i.card.name.toLowerCase().includes(q))
.sort((a, b) => (b.effective_overall ?? b.card.overall) - (a.effective_overall ?? a.card.overall))
.slice(0, 60);
el('picker-list').innerHTML = cards.map(i => {
const c = i.card;
const ovr = i.effective_overall ?? c.overall;
return `<div onclick="pickSlotCard('${i.owned_card_id}')" style="
display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;
background:#21262d;cursor:pointer;font-size:0.82rem"
onmouseover="this.style.background='#30363d'" onmouseout="this.style.background='#21262d'">
<span style="font-weight:700;color:#f5a623;width:28px;text-align:right">${ovr}</span>
<span style="font-size:0.65rem;color:#8b949e;width:28px;text-transform:uppercase">${c.position}</span>
<span style="flex:1;font-weight:600">${c.name}</span>
<span style="font-size:0.7rem;color:#8b949e">${c.club}</span>
</div>`;
}).join('') || '<span class="empty">No matching cards.</span>';
}
function pickSlotCard(ownedId) {
if (!pickerTargetSlot) return;
const { type, index } = pickerTargetSlot;
(type === 'starter' ? builderStarters : builderBench)[index] = ownedId;
closePicker();
rebuildSlots();
}
function clearSlot(type, index) {
delete (type === 'starter' ? builderStarters : builderBench)[index];
rebuildSlots();
}
function clearBuilder() {
Object.keys(builderStarters).forEach(k => delete builderStarters[k]);
Object.keys(builderBench).forEach(k => delete builderBench[k]);
rebuildSlots();
}
async function saveSquad() {
const formation = el('builder-formation').value;
const name = el('builder-name').value.trim() || 'My Squad';
const positions = FORMATION_POSITIONS[formation] || FORMATION_POSITIONS['4-4-2'];
const players = [];
for (let i = 0; i < positions.length; i++) {
if (builderStarters[i]) {
players.push({ owned_card_id: builderStarters[i], position_index: i, is_captain: i === 0, is_on_bench: false });
}
}
for (let i = 0; i < BENCH_LABELS.length; i++) {
if (builderBench[i]) {
players.push({ owned_card_id: builderBench[i], position_index: 11 + i, is_captain: false, is_on_bench: true });
}
}
if (players.filter(p => !p.is_on_bench).length !== 11) {
showToast('Fill all 11 starter slots before saving', true);
return;
}
try {
await api('POST', '/squad', { name, formation, players });
showToast(`Squad "${name}" saved!`);
squadMode('view');
loadSquad();
} catch (e) { showToast(e.message, true); }
}
// ── Draft ─────────────────────────────────────────────────────────────────────
let activeDraftSession = null;
async function loadDraft() {
el('draft-status').innerHTML = '<span class="loading">Loading…</span>';
el('draft-candidates').innerHTML = '';
el('draft-picks').innerHTML = '';
activeDraftSession = null;
// Try to find an active session via history approach — Core has no "list active" endpoint,
// so we just show the start UI and let the user start or continue manually.
el('draft-status').innerHTML = `
<div style="font-size:0.85rem;color:#8b949e;margin-bottom:12px">
Select difficulty and start a new draft to pick 11 players (GK → ST).
Each slot shows 5 candidates; pick one to advance.
</div>`;
}
async function startDraft() {
const diff = el('draft-difficulty').value;
try {
const data = await api('POST', `/draft/start?difficulty=${diff}`);
activeDraftSession = data;
renderDraftSession(data);
el('draft-start-btn').style.display = 'none';
el('draft-abandon-btn').style.display = '';
showToast('Draft started!');
} catch (e) { showToast(e.message, true); }
}
async function abandonDraft() {
if (!activeDraftSession?.session_id) return;
if (!confirm('Abandon this draft? No rewards will be granted.')) return;
try {
await api('POST', `/draft/sessions/${activeDraftSession.session_id}/abandon`);
activeDraftSession = null;
el('draft-start-btn').style.display = '';
el('draft-abandon-btn').style.display = 'none';
el('draft-status').innerHTML = '<span class="empty">Draft abandoned.</span>';
el('draft-candidates').innerHTML = '';
el('draft-picks').innerHTML = '';
showToast('Draft abandoned.');
} catch (e) { showToast(e.message, true); }
}
async function pickDraftCard(cardId) {
if (!activeDraftSession?.session_id) return;
try {
const data = await api('POST', `/draft/sessions/${activeDraftSession.session_id}/pick`, { card_id: cardId });
activeDraftSession = data;
renderDraftSession(data);
if (data.just_completed) {
const r = data.reward || {};
showToast(`Draft complete! ${(r.coins || 0).toLocaleString()} coins earned (avg OVR ${r.squad_rating || 0})`);
el('draft-abandon-btn').style.display = 'none';
api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {});
}
} catch (e) { showToast(e.message, true); }
}
function renderDraftSession(data) {
const picks = data.picks || [];
const candidates = data.candidates || [];
const progress = data.progress || {};
const status = data.status;
const isComplete = status === 'completed';
// Dots progress bar
const dots = Array.from({length: progress.total || 11}, (_, i) =>
`<span class="pick-dot ${i < progress.filled ? 'filled' : ''}"></span>`
).join('');
el('draft-status').innerHTML = `
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
<strong>${isComplete ? 'Draft Complete!' : `Picking: ${data.current_position || '—'}`}</strong>
<span style="font-size:0.8rem;color:#8b949e">${progress.filled || 0}/${progress.total || 11}</span>
</div>
<div class="pick-slots" style="margin-bottom:12px">${dots}</div>
${isComplete ? `
<div style="background:#0d2818;border-radius:6px;padding:12px;margin-bottom:12px">
<div style="font-size:0.9rem"><strong>Reward:</strong> ${(data.reward?.coins || 0).toLocaleString()} coins
${data.reward?.pack_id ? ' + ' + data.reward.pack_id : ''}
· Squad Rating: ${data.reward?.squad_rating || data.squad_rating || '—'}</div>
</div>
<button class="btn btn-primary btn-sm" onclick="startDraft()">Start Another Draft</button>` : ''}`;
// Candidates
if (!isComplete && candidates.length) {
el('draft-candidates').innerHTML = candidates.map(cand => {
const c = cand.card;
if (!c) return '';
return `<div class="draft-candidate" onclick="pickDraftCard('${cand.card_id}')">
<span class="card-rarity ${rarityClass(c.rarity)}" style="position:static;display:inline-block;margin-bottom:4px">${(c.rarity||'').toUpperCase()}</span>
<div class="card-ovr" style="font-size:1.3rem">${c.overall}</div>
<div style="font-size:0.7rem;color:#8b949e;text-transform:uppercase">${c.position}</div>
<div style="font-size:0.85rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${c.name}</div>
<div style="font-size:0.7rem;color:#8b949e">${c.club}</div>
<div style="margin-top:6px;font-size:0.7rem">
${statBar('PAC',c.pace)}${statBar('SHO',c.shooting)}${statBar('PAS',c.passing)}
</div>
</div>`;
}).join('');
} else if (isComplete) {
el('draft-candidates').innerHTML = '';
el('draft-start-btn').style.display = '';
el('draft-abandon-btn').style.display = 'none';
}
// Picks list
el('draft-picks').innerHTML = picks.map(p => {
const c = p.card;
if (!c) return `<div class="squad-slot"><div class="slot-pos">${p.position}</div><span style="color:#8b949e;font-size:0.8rem">—</span></div>`;
return `<div class="squad-slot">
<div class="slot-pos">${p.position}</div>
<div class="card-ovr" style="font-size:1.1rem">${c.overall}</div>
<div style="font-size:0.8rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${c.name}</div>
<div style="font-size:0.7rem;color:#8b949e">${c.position}</div>
</div>`;
}).join('');
}
// ── Market ────────────────────────────────────────────────────────────────────
let allMarketListings = [];
async function loadMarket() {
await Promise.all([loadMarketListings(), loadMyListings()]);
// Populate sell card dropdown from collection
if (!allCards.length) {
try { const d = await api('GET', '/collection'); allCards = d.collection || []; } catch (_) {}
}
const sel = el('sell-card-select');
sel.innerHTML = '<option value="">— choose card —</option>' +
allCards
.filter(i => !i.is_loan)
.sort((a, b) => (b.effective_overall ?? b.card.overall) - (a.effective_overall ?? a.card.overall))
.map(i => {
const c = i.card;
const ovr = i.effective_overall ?? c.overall;
return `<option value="${i.owned_card_id}">${ovr} ${c.name} (${c.position})</option>`;
}).join('');
}
async function loadMarketListings() {
try {
const data = await api('GET', '/market');
allMarketListings = data.listings || [];
filterMarket();
} catch (e) { el('market-grid').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
function filterMarket() {
const q = (el('m-search')?.value || '').toLowerCase();
const filtered = q
? allMarketListings.filter(l => l.card && l.card.name.toLowerCase().includes(q))
: allMarketListings;
if (!filtered.length) {
el('market-grid').innerHTML = '<span class="empty">No listings found. Click "Refresh Listings" to populate the market.</span>';
return;
}
el('market-grid').innerHTML = filtered.map(l => {
const c = l.card;
if (!c) return '';
return `<div class="market-item">
<span class="card-rarity ${rarityClass(c.rarity)}" style="position:static;display:inline-block;margin-bottom:4px">${(c.rarity||'').toUpperCase()}</span>
<div class="card-ovr" style="font-size:1.3rem">${c.overall}</div>
<div style="font-size:0.7rem;color:#8b949e;text-transform:uppercase">${c.position}</div>
<div style="font-size:0.85rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${c.name}</div>
<div style="font-size:0.7rem;color:#8b949e">${c.club} · ${c.nation}</div>
<div class="market-price">${(l.listing?.buy_now_price || l.buy_now_price || 0).toLocaleString()} coins</div>
<div class="market-seller">${l.listing?.seller_name || l.seller_name || 'NPC'}</div>
<button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%"
onclick="buyListing('${l.listing?.id || l.id}', this)">Buy</button>
</div>`;
}).join('');
}
async function refreshMarket() {
try {
await api('POST', '/market/refresh');
showToast('Market refreshed!');
await loadMarketListings();
} catch (e) { showToast(e.message, true); }
}
async function buyListing(listingId, btn) {
btn.disabled = true; btn.textContent = 'Buying…';
try {
const r = await api('POST', '/market/buy', { listing_id: listingId });
showToast(`Bought ${r.card?.name || 'card'} for ${(r.price || 0).toLocaleString()} coins`);
api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {});
allCards = []; // invalidate collection cache
await loadMarketListings();
} catch (e) { showToast(e.message, true); btn.disabled = false; btn.textContent = 'Buy'; }
}
async function sellCard() {
const ownedCardId = el('sell-card-select').value;
const price = parseInt(el('sell-price').value, 10);
if (!ownedCardId) { showToast('Select a card first', true); return; }
if (!price || price < 100) { showToast('Price must be at least 100 coins', true); return; }
try {
await api('POST', '/market/sell', { owned_card_id: ownedCardId, buy_now_price: price });
showToast('Card listed for sale!');
allCards = [];
await loadMyListings();
} catch (e) { showToast(e.message, true); }
}
async function loadMyListings() {
try {
const data = await api('GET', '/market/my-listings');
const listings = data.listings || [];
if (!listings.length) {
el('my-listings').innerHTML = '<span class="empty">No active listings.</span>';
return;
}
el('my-listings').innerHTML = listings.map(l => {
const c = l.card;
return `<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:8px;display:flex;align-items:center;gap:12px">
<div>
<div style="font-size:0.85rem;font-weight:600">${c?.name || l.listing?.card_id || '—'}</div>
<div style="font-size:0.75rem;color:#8b949e">${c?.position || ''} · ${c?.club || ''}</div>
</div>
<div style="margin-left:auto;text-align:right">
<div class="market-price" style="font-size:0.9rem">${(l.listing?.buy_now_price || 0).toLocaleString()} coins</div>
<button class="btn btn-danger btn-sm" style="margin-top:4px"
onclick="cancelListing('${l.listing?.id || ''}', this)">Cancel</button>
</div>
</div>`;
}).join('');
} catch (e) { el('my-listings').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
async function cancelListing(listingId, btn) {
btn.disabled = true;
try {
await api('DELETE', `/market/listings/${listingId}`);
showToast('Listing cancelled');
allCards = [];
await loadMyListings();
} catch (e) { showToast(e.message, true); btn.disabled = false; }
}
// ── Events ────────────────────────────────────────────────────────────────────
async function loadEvents() {
try {
const data = await api('GET', '/events');
const events = data.events || [];
if (!events.length) {
el('events-grid').innerHTML = '<span class="empty">No events defined in data/events.json.</span>';
return;
}
el('events-grid').innerHTML = events.map(ev => {
const active = ev.is_active;
const effects = ev.effects || {};
const bonuses = [
effects.score_multiplier && effects.score_multiplier !== 1 ? `Score ×${effects.score_multiplier}` : '',
effects.coin_multiplier && effects.coin_multiplier !== 1 ? `Coins ×${effects.coin_multiplier}` : '',
(effects.bonus_market_cards || []).length ? `+${effects.bonus_market_cards.length} market cards` : '',
effects.pack_discount_pct ? `-${effects.pack_discount_pct}% packs` : '',
].filter(Boolean).join(' · ');
return `<div class="event-card${active ? ' active' : ''}">
<div class="event-name">${ev.name}</div>
<div class="event-desc">${ev.description || ''}</div>
${bonuses ? `<div style="font-size:0.75rem;color:#f5a623;margin-bottom:8px">${bonuses}</div>` : ''}
<div style="display:flex;align-items:center;justify-content:space-between">
<span class="event-status ${active ? 'on' : 'off'}">${active ? '● Active' : '○ Inactive'}</span>
${active
? `<button class="btn btn-secondary btn-sm" onclick="toggleEvent('${ev.id}', false, this)">Deactivate</button>`
: `<button class="btn btn-primary btn-sm" onclick="toggleEvent('${ev.id}', true, this)">Activate</button>`}
</div>
</div>`;
}).join('');
} catch (e) { el('events-grid').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
async function toggleEvent(eventId, activate, btn) {
btn.disabled = true;
try {
const path = activate ? `/events/${eventId}/activate` : `/events/${eventId}/deactivate`;
await api('POST', path);
showToast(activate ? 'Event activated!' : 'Event deactivated');
loadEvents();
} catch (e) { showToast(e.message, true); btn.disabled = false; }
}
// ── Notifications ─────────────────────────────────────────────────────────────
async function loadNotifications() {
try {
const data = await api('GET', '/notifications');
const notifs = data.notifications || [];
el('notif-badge').textContent = notifs.length > 0 ? ` (${notifs.length})` : '';
if (!notifs.length) {
el('notif-list').innerHTML = '<span class="empty">No notifications right now.</span>';
return;
}
el('notif-list').innerHTML = notifs.map(n => `
<div class="notif-item ${n.type}">
<div class="notif-title">${n.title}</div>
<div class="notif-msg">${n.message}</div>
</div>`).join('');
} catch (e) { el('notif-list').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
// ── Init ─────────────────────────────────────────────────────────────────────
window.addEventListener('DOMContentLoaded', () => {
loadClub();
// Load notification badge count silently
api('GET', '/notifications').then(d => {
const n = (d.notifications || []).length;
el('notif-badge').textContent = n > 0 ? ` (${n})` : '';
}).catch(() => {});
});
</script>
</body>
</html>