diff --git a/src/dashboard.html b/src/dashboard.html index 6f1c673..90328f8 100644 --- a/src/dashboard.html +++ b/src/dashboard.html @@ -275,6 +275,7 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; +
@@ -639,6 +640,15 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; +
+
+

Achievements

+
+
+ Loading… +
+
+

Notifications

@@ -698,7 +708,7 @@ function showTab(name) { market: loadMarket, events: loadEvents, matches: loadMatches, store: loadStore, sbc: loadSbc, statistics: loadStatistics, catalog: loadCatalog, settings: loadSettings, - notifications: loadNotifications })[name]?.(); + notifications: loadNotifications, achievements: loadAchievements })[name]?.(); } function rarityClass(r) { @@ -1189,6 +1199,8 @@ async function submitMatch() { if (levelUps.length) { showToast(`Level Up! You are now level ${levelUps[levelUps.length - 1].new_level}`); } + const achUnlocked = r.achievements_unlocked || []; + achUnlocked.forEach(a => showToast(`Achievement unlocked: ${a.icon || ''} ${a.title}`)); api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {}); await loadMatchHistory(); } catch (e) { showToast(e.message, true); } @@ -2080,6 +2092,59 @@ async function toggleEvent(eventId, activate, btn) { // ── Notifications ───────────────────────────────────────────────────────────── +// ── Achievements ───────────────────────────────────────────────────────────── + +const ACH_RARITY_COLOR = { common: '#8b949e', rare: '#58a6ff', epic: '#bc8cff', legendary: '#f5a623' }; + +async function loadAchievements() { + try { + const data = await api('GET', '/achievements'); + const achs = data.achievements || []; + const earned = data.earned || 0; + const total = data.total || achs.length; + + el('ach-progress').textContent = `${earned} / ${total} earned`; + el('ach-badge').textContent = earned > 0 ? ` (${earned})` : ''; + + if (!achs.length) { + el('ach-grid').innerHTML = 'No achievements defined.'; + return; + } + + // Sort: unlocked first (by unlock date desc), then locked alphabetically + const unlocked = achs.filter(a => a.unlocked).sort((a, b) => (b.unlocked_at || '').localeCompare(a.unlocked_at || '')); + const locked = achs.filter(a => !a.unlocked).sort((a, b) => a.title.localeCompare(b.title)); + + el('ach-grid').innerHTML = [...unlocked, ...locked].map(a => { + const rc = ACH_RARITY_COLOR[a.rarity] || '#8b949e'; + const opacity = a.unlocked ? '1' : '0.45'; + const badge = a.unlocked + ? `✓ Unlocked${a.unlocked_at ? ' ' + new Date(a.unlocked_at).toLocaleDateString() : ''}` + : `Locked`; + return ` +
+ ${a.icon} +
+
+ ${a.title} + ${a.rarity} +
+
${a.description}
+
+ ${badge} + +${(a.reward_coins||0).toLocaleString()} coins +
+
+
`; + }).join(''); + } catch (e) { el('ach-grid').innerHTML = `
${e.message}
`; } +} + +// ── Notifications ───────────────────────────────────────────────────────────── + const NOTIF_ICONS = { level_up: '⬆', objective_complete: '✓', diff --git a/tests/proxy_test.rs b/tests/proxy_test.rs index e18a27a..a2df291 100644 --- a/tests/proxy_test.rs +++ b/tests/proxy_test.rs @@ -251,6 +251,7 @@ async fn test_dashboard_contains_key_sections() { assert!(html.contains("tab-catalog"), "card catalog tab missing"); assert!(html.contains("tab-settings"), "settings tab missing"); assert!(html.contains("tab-notifications"), "notifications tab missing"); + assert!(html.contains("tab-achievements"), "achievements tab missing"); } #[tokio::test]