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>
This commit is contained in:
funman300
2026-06-25 17:42:35 -07:00
parent 8087564df1
commit ba1b0a09f2
+49 -5
View File
@@ -531,6 +531,10 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
<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>
@@ -1138,15 +1142,23 @@ async function submitMatch() {
});
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_triggered || []).length ? `<br>Objectives: ${r.objectives_triggered.join(', ')}` : ''}
${r.season ? `<br>Season pts: ${r.season.season_points}` : ''}
${(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); }
@@ -1480,12 +1492,15 @@ async function loadSettings() {
api('GET', '/profile'),
api('GET', '/settings'),
]);
const xpForNext = 1000;
const xpPct = Math.min(100, Math.round(((profile.xp || 0) % xpForNext) / xpForNext * 100));
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()}</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>
`;
@@ -1563,6 +1578,35 @@ async function loadSquad() {
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 ────────────────────────────────────────────────────────────