diff --git a/src/dashboard.html b/src/dashboard.html index 13851a7..9255969 100644 --- a/src/dashboard.html +++ b/src/dashboard.html @@ -173,6 +173,35 @@ 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; } +/* ── 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; } @@ -235,6 +264,8 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; + + @@ -346,6 +377,66 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; + +
+
+ + + + + + + + +
+
+
Loading…
+
+ +
+
+ + +
+
+
+

Profile

+
Loading…
+
+
+

Game Settings

+
+ + + + + + +
+
+
+
+
@@ -424,18 +515,59 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
-
-
-
-

Active Squad

- +
+

Squad

+ + + +
+ +
+
+
+
+
Loading…
+
+
+

Chemistry

+
Loading…
-
-
Loading…
-
-

Chemistry

-
Loading…
+
+ +
@@ -557,6 +689,7 @@ function showTab(name) { 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]?.(); } @@ -1265,6 +1398,117 @@ async function loadStatistics() { } catch (e) { el('stats-history').innerHTML = `
${e.message}
`; } } +// ── Card Catalog ────────────────────────────────────────────────────────────── + +let catalogCards = []; +let catalogOwned = new Set(); +let catalogLimit = 80; + +async function loadCatalog() { + el('catalog-grid').innerHTML = 'Loading…'; + 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 = `
${e.message}
`; } +} + +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 `
+ ${owned ? '
Owned' : ''} + ${(c.rarity||'').toUpperCase()} +
${c.overall}
+
${c.position}
+
${c.name}
+
${c.club}
+
+ ${statBar('PAC',c.pace)}${statBar('SHO',c.shooting)}${statBar('PAS',c.passing)} + ${statBar('DRI',c.dribbling)}${statBar('DEF',c.defending)}${statBar('PHY',c.physical)} +
+
`; + }).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 = 1000; + const xpPct = Math.min(100, Math.round(((profile.xp || 0) % xpForNext) / xpForNext * 100)); + el('profile-card').innerHTML = ` +
Username${profile.username || '—'}
+
Level${profile.level ?? 1}
+
XP${(profile.xp || 0).toLocaleString()}
+
+
Profile ID: ${profile.id}
+ `; + 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 = `
${e.message}
`; + } +} + +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() { @@ -1321,6 +1565,155 @@ async function loadSquad() { } } +// ── 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 `
+
${label}
+ +
${ovr}
+
${c.name}
+
${c.position}
+
`; + } + return `
+
${label}
+
+ Pick player
+
`; +} + +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 `
+ ${ovr} + ${c.position} + ${c.name} + ${c.club} +
`; + }).join('') || 'No matching cards.'; +} + +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; diff --git a/tests/proxy_test.rs b/tests/proxy_test.rs index d71310f..e18a27a 100644 --- a/tests/proxy_test.rs +++ b/tests/proxy_test.rs @@ -248,6 +248,8 @@ async fn test_dashboard_contains_key_sections() { 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-catalog"), "card catalog tab missing"); + assert!(html.contains("tab-settings"), "settings tab missing"); assert!(html.contains("tab-notifications"), "notifications tab missing"); }