Phase 15: add Matches, Pack Store, SBC, Statistics tabs to dashboard
Matches — difficulty selector, generate opponent (shows AI squad rating and formation), quick-score buttons (3-0 / 1-1 / 0-2), submit custom score via POST /matches/result, inline reward display (coins, XP, objective triggers, season points), live match history list. Pack Store — lists all purchasable definitions from GET /packs/store (name, description, cost, card count) with one-click Buy that calls POST /packs/buy; coin balance refreshes after purchase. SBC — renders all challenges with requirements as pills (min OVR, required nations/leagues/clubs, same-club counts), card picker (select from owned collection, click pill to remove), Submit fires POST /sbc/submit and shows reward or validation failures; cards re-loaded from Core after a successful submission. Statistics — career stat grid (matches, W/D/L, win rate, goals, coin totals, streaks, packs, SBCs), goals-by-position bar chart, paginated match log with outcome badges and date, sourced from GET /statistics and GET /statistics/history. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+423
-1
@@ -173,6 +173,38 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
.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; }
|
||||
|
||||
/* ── 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; }
|
||||
@@ -203,6 +235,10 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
<button onclick="showTab('objectives')">Objectives</button>
|
||||
<button onclick="showTab('division')">Division</button>
|
||||
<button onclick="showTab('champs')">FUT Champs</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>
|
||||
@@ -310,6 +346,82 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
</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 class="two-col">
|
||||
@@ -443,7 +555,8 @@ function showTab(name) {
|
||||
({ club: loadClub, collection: loadCollection, packs: loadPacks,
|
||||
objectives: loadObjectives, division: loadDivision,
|
||||
champs: loadChamps, squad: loadSquad, draft: loadDraft,
|
||||
market: loadMarket, events: loadEvents,
|
||||
market: loadMarket, events: loadEvents, matches: loadMatches,
|
||||
store: loadStore, sbc: loadSbc, statistics: loadStatistics,
|
||||
notifications: loadNotifications })[name]?.();
|
||||
}
|
||||
|
||||
@@ -843,6 +956,315 @@ async function claimChampsReward(sessionId) {
|
||||
} 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';
|
||||
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_triggered || []).length ? `<br>Objectives: ${r.objectives_triggered.join(', ')}` : ''}
|
||||
${r.season ? `<br>Season pts: ${r.season.season_points}` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
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>`; }
|
||||
}
|
||||
|
||||
// ── Squad ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadSquad() {
|
||||
|
||||
@@ -244,6 +244,10 @@ async fn test_dashboard_contains_key_sections() {
|
||||
assert!(html.contains("tab-draft"), "draft tab missing");
|
||||
assert!(html.contains("tab-market"), "market tab missing");
|
||||
assert!(html.contains("tab-events"), "events tab missing");
|
||||
assert!(html.contains("tab-matches"), "matches tab missing");
|
||||
assert!(html.contains("tab-store"), "pack store tab missing");
|
||||
assert!(html.contains("tab-sbc"), "sbc tab missing");
|
||||
assert!(html.contains("tab-statistics"), "statistics tab missing");
|
||||
assert!(html.contains("tab-notifications"), "notifications tab missing");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user