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:
+49
-5
@@ -531,6 +531,10 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
|
|||||||
<div>
|
<div>
|
||||||
<h2>Chemistry</h2>
|
<h2>Chemistry</h2>
|
||||||
<div id="squad-chemistry"><span class="loading">Loading…</span></div>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1138,15 +1142,23 @@ async function submitMatch() {
|
|||||||
});
|
});
|
||||||
const outcome = gf > ga ? 'Win' : gf === ga ? 'Draw' : 'Loss';
|
const outcome = gf > ga ? 'Win' : gf === ga ? 'Draw' : 'Loss';
|
||||||
const outcomeColor = gf > ga ? '#3fb950' : gf === ga ? '#8b949e' : '#f85149';
|
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 = `
|
el('match-result-display').innerHTML = `
|
||||||
<div style="background:#0d1117;border-radius:6px;padding:12px;border-left:3px solid ${outcomeColor}">
|
<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-weight:700;color:${outcomeColor}">${outcome} ${gf}–${ga}</div>
|
||||||
<div style="font-size:0.8rem;margin-top:4px">
|
<div style="font-size:0.8rem;margin-top:4px">
|
||||||
+${(r.coins_awarded || 0).toLocaleString()} coins · +${r.xp_awarded || 0} XP
|
+${(r.coins_awarded || 0).toLocaleString()} coins · +${r.xp_awarded || 0} XP
|
||||||
${(r.objectives_triggered || []).length ? `<br>Objectives: ${r.objectives_triggered.join(', ')}` : ''}
|
${(r.objectives_updated || []).length ? `<br>Objectives: ${r.objectives_updated.join(', ')}` : ''}
|
||||||
${r.season ? `<br>Season pts: ${r.season.season_points}` : ''}
|
${r.season_end ? `<br>Season ended: Division ${r.season_end.new_division ?? '—'}` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
${levelUpHtml}
|
||||||
</div>`;
|
</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(() => {});
|
api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {});
|
||||||
await loadMatchHistory();
|
await loadMatchHistory();
|
||||||
} catch (e) { showToast(e.message, true); }
|
} catch (e) { showToast(e.message, true); }
|
||||||
@@ -1480,12 +1492,15 @@ async function loadSettings() {
|
|||||||
api('GET', '/profile'),
|
api('GET', '/profile'),
|
||||||
api('GET', '/settings'),
|
api('GET', '/settings'),
|
||||||
]);
|
]);
|
||||||
const xpForNext = 1000;
|
const xpForNext = profile.xp_for_next_level || 1000;
|
||||||
const xpPct = Math.min(100, Math.round(((profile.xp || 0) % xpForNext) / xpForNext * 100));
|
const xpEarned = xpForNext - (profile.xp_to_next_level || 0);
|
||||||
|
const xpPct = Math.min(100, Math.round((xpEarned / xpForNext) * 100));
|
||||||
el('profile-card').innerHTML = `
|
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">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">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 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>
|
<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-grid').innerHTML = `<div class="error-msg">${e.message}</div>`;
|
||||||
el('squad-chemistry').innerHTML = '';
|
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 ────────────────────────────────────────────────────────────
|
// ── Squad Builder ────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user