Phase 14: add Squad, Draft, Market, Events tabs to dashboard

Four new tabs complete the dashboard UI for all existing Core endpoints:

Squad — view active squad with per-player chemistry dots and team
chemistry bar (colour-coded green/amber/red).

Draft — start a session with difficulty selector, pick from 5 candidates
per slot through all 11 positions, see picks accumulate in real time,
reward shown on completion.

Market — browse NPC listings with live search filter, one-click buy,
sell own cards with price input, cancel own listings; Refresh button
repopulates NPC inventory via POST /market/refresh.

Events — list all data-driven events with active/inactive state, bonus
effects summary (score/coin multipliers, extra market cards, pack
discounts), Activate/Deactivate buttons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 17:22:26 -07:00
parent eae3e72fd9
commit aabbe775b3
2 changed files with 478 additions and 1 deletions
+474 -1
View File
@@ -141,6 +141,47 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
max-width: 480px; width: 90%; max-height: 80vh; overflow-y: auto; }
.modal h3 { color: #f5a623; margin-bottom: 16px; }
.opened-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; margin-top: 12px; }
/* ── Squad ── */
.squad-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; margin-top: 12px; }
.squad-slot { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 10px; }
.squad-slot .slot-pos { font-size: 0.7rem; text-transform: uppercase; color: #8b949e; margin-bottom: 4px; }
.chem-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 2px; }
.chem-dot.full { background: #3fb950; }
.chem-dot.partial { background: #d29922; }
.chem-dot.none { background: #f85149; }
.chem-bar { height: 8px; background: #21262d; border-radius: 4px; overflow: hidden; margin: 6px 0; }
.chem-bar-fill { height: 100%; border-radius: 4px; background: #3fb950; }
.formation-badge { font-size: 0.75rem; background: #1c3250; color: #93c5fd; padding: 2px 8px; border-radius: 10px; }
/* ── Draft ── */
.draft-candidates { display: grid; grid-template-columns: repeat(auto-fill, minmax(165px, 1fr)); gap: 10px; margin-top: 12px; }
.draft-candidate { background: #161b22; border: 2px solid #30363d; border-radius: 8px; padding: 10px;
cursor: pointer; transition: border-color 0.15s; }
.draft-candidate:hover { border-color: #f5a623; }
.pick-slots { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 8px; }
.pick-dot { width: 12px; height: 12px; border-radius: 50%; background: #21262d; border: 1px solid #30363d; }
.pick-dot.filled { background: #f5a623; border-color: #f5a623; }
.draft-progress { font-size: 0.8rem; color: #8b949e; margin-bottom: 12px; }
/* ── Market ── */
.market-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; margin-top: 12px; }
.market-item { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px; }
.market-price { color: #f5a623; font-weight: 700; font-size: 1rem; margin: 6px 0; }
.market-seller { font-size: 0.7rem; color: #8b949e; }
.sell-form { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; max-width: 480px; }
.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; }
/* ── 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; }
.event-card.active { border-color: #f5a623; }
.event-name { font-size: 0.95rem; font-weight: 600; margin-bottom: 4px; }
.event-desc { font-size: 0.8rem; color: #8b949e; margin-bottom: 10px; }
.event-status { font-size: 0.75rem; font-weight: 600; }
.event-status.on { color: #3fb950; }
.event-status.off { color: #8b949e; }
</style>
</head>
<body>
@@ -162,6 +203,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('squad')">Squad</button>
<button onclick="showTab('draft')">Draft</button>
<button onclick="showTab('market')">Market</button>
<button onclick="showTab('events')">Events</button>
<button onclick="showTab('notifications')">Notifications <span id="notif-badge"></span></button>
</nav>
@@ -265,6 +310,86 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
</div>
</div>
<!-- ── Squad tab ─────────────────────────────────────────────────────────── -->
<div id="tab-squad" class="tab-panel">
<div class="two-col">
<div>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px">
<h2 style="border:none;margin:0">Active Squad</h2>
<button class="btn btn-secondary btn-sm" onclick="loadSquad()">Refresh</button>
</div>
<div id="squad-header"></div>
<div id="squad-grid" class="squad-grid"><span class="loading">Loading…</span></div>
</div>
<div>
<h2>Chemistry</h2>
<div id="squad-chemistry"><span class="loading">Loading…</span></div>
</div>
</div>
</div>
<!-- ── Draft tab ──────────────────────────────────────────────────────────── -->
<div id="tab-draft" class="tab-panel">
<div class="two-col">
<div>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px;flex-wrap:wrap">
<h2 style="border:none;margin:0">FUT Draft</h2>
<select id="draft-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-primary btn-sm" id="draft-start-btn" onclick="startDraft()">New Draft</button>
<button class="btn btn-danger btn-sm" id="draft-abandon-btn" style="display:none" onclick="abandonDraft()">Abandon</button>
</div>
<div id="draft-status"></div>
<div id="draft-candidates" class="draft-candidates"></div>
</div>
<div>
<h2>Picks So Far</h2>
<div id="draft-picks" class="squad-grid"></div>
</div>
</div>
</div>
<!-- ── Market tab ────────────────────────────────────────────────────────── -->
<div id="tab-market" class="tab-panel">
<div class="section">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px;flex-wrap:wrap">
<h2 style="border:none;margin:0">Transfer Market</h2>
<button class="btn btn-secondary btn-sm" onclick="refreshMarket()">Refresh Listings</button>
<input id="m-search" placeholder="Search player…" oninput="filterMarket()"
style="background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 10px;border-radius:6px;font-size:0.8rem">
</div>
<div id="market-grid" class="market-grid"><span class="loading">Loading…</span></div>
</div>
<div class="section">
<h2>Sell a Card</h2>
<div class="sell-form">
<label style="font-size:0.8rem;color:#8b949e">Select card from collection</label>
<select id="sell-card-select"><option value="">— choose card —</option></select>
<label style="font-size:0.8rem;color:#8b949e">Buy-now price (coins)</label>
<input id="sell-price" type="number" min="100" step="50" value="1000" placeholder="Price">
<button class="btn btn-primary btn-sm" onclick="sellCard()">List for Sale</button>
</div>
<div class="section" style="margin-top:16px">
<h2>My Listings</h2>
<div id="my-listings"><span class="loading">Loading…</span></div>
</div>
</div>
</div>
<!-- ── Events tab ────────────────────────────────────────────────────────── -->
<div id="tab-events" class="tab-panel">
<p style="font-size:0.85rem;color:#8b949e;margin-bottom:16px">
Activate special events to unlock bonus cards in the market, score multipliers, and extra objectives.
All events are offline-only and have no expiry unless you deactivate them.
</p>
<div id="events-grid" class="events-grid"><span class="loading">Loading…</span></div>
</div>
<!-- ── Notifications tab ─────────────────────────────────────────────────── -->
<div id="tab-notifications" class="tab-panel">
<div id="notif-list" class="notif-list"><span class="loading">Loading…</span></div>
@@ -317,7 +442,9 @@ function showTab(name) {
event.target.classList.add('active');
({ club: loadClub, collection: loadCollection, packs: loadPacks,
objectives: loadObjectives, division: loadDivision,
champs: loadChamps, notifications: loadNotifications })[name]?.();
champs: loadChamps, squad: loadSquad, draft: loadDraft,
market: loadMarket, events: loadEvents,
notifications: loadNotifications })[name]?.();
}
function rarityClass(r) {
@@ -716,6 +843,352 @@ async function claimChampsReward(sessionId) {
} catch (e) { showToast(e.message, true); }
}
// ── Squad ─────────────────────────────────────────────────────────────────────
async function loadSquad() {
try {
const data = await api('GET', '/squad');
const squad = data.squad;
const players = data.players || [];
const chem = data.chemistry || {};
el('squad-header').innerHTML = `
<div style="display:flex;gap:10px;align-items:center;margin-bottom:12px">
<strong>${squad?.name || 'My Squad'}</strong>
<span class="formation-badge">${squad?.formation || '—'}</span>
</div>`;
if (!players.length) {
el('squad-grid').innerHTML = '<span class="empty">No squad saved yet. Use /squad POST from the game or API.</span>';
el('squad-chemistry').innerHTML = '';
return;
}
const playerChems = chem.players || {};
el('squad-grid').innerHTML = players.map(p => {
const card = p.card;
const pChem = playerChems[p.owned_card_id] ?? 0;
const dots = Array.from({length: 10}, (_, i) =>
`<span class="chem-dot ${i < pChem ? (pChem >= 7 ? 'full' : 'partial') : 'none'}"></span>`
).join('');
return `<div class="squad-slot">
<div class="slot-pos">${p.position_label || 'Player'}${p.is_on_bench ? ' (sub)' : ''}</div>
${card ? `
<div class="card-ovr" style="font-size:1.2rem">${card.overall}</div>
<div style="font-size:0.8rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${card.name}</div>
<div style="font-size:0.7rem;color:#8b949e">${card.position} · ${card.club}</div>
<div style="margin-top:4px;font-size:0.65rem;color:#8b949e">Chem: ${pChem}/10</div>
<div>${dots}</div>
` : '<span style="color:#8b949e;font-size:0.8rem">Empty</span>'}
</div>`;
}).join('');
const total = chem.total ?? 0;
const pct = total;
const chemColor = total >= 80 ? '#3fb950' : total >= 50 ? '#d29922' : '#f85149';
el('squad-chemistry').innerHTML = `
<div style="font-size:2.5rem;font-weight:800;color:${chemColor}">${total}<span style="font-size:1rem;color:#8b949e">/100</span></div>
<div class="chem-bar" style="margin:8px 0 16px"><div class="chem-bar-fill" style="width:${pct}%;background:${chemColor}"></div></div>
<p style="font-size:0.8rem;color:#8b949e">
Chemistry is earned through same-club, same-league, and same-nation links between adjacent players.
Each player earns up to 10 chem points; total is capped at 100.
</p>`;
} catch (e) {
el('squad-grid').innerHTML = `<div class="error-msg">${e.message}</div>`;
el('squad-chemistry').innerHTML = '';
}
}
// ── Draft ─────────────────────────────────────────────────────────────────────
let activeDraftSession = null;
async function loadDraft() {
el('draft-status').innerHTML = '<span class="loading">Loading…</span>';
el('draft-candidates').innerHTML = '';
el('draft-picks').innerHTML = '';
activeDraftSession = null;
// Try to find an active session via history approach — Core has no "list active" endpoint,
// so we just show the start UI and let the user start or continue manually.
el('draft-status').innerHTML = `
<div style="font-size:0.85rem;color:#8b949e;margin-bottom:12px">
Select difficulty and start a new draft to pick 11 players (GK → ST).
Each slot shows 5 candidates; pick one to advance.
</div>`;
}
async function startDraft() {
const diff = el('draft-difficulty').value;
try {
const data = await api('POST', `/draft/start?difficulty=${diff}`);
activeDraftSession = data;
renderDraftSession(data);
el('draft-start-btn').style.display = 'none';
el('draft-abandon-btn').style.display = '';
showToast('Draft started!');
} catch (e) { showToast(e.message, true); }
}
async function abandonDraft() {
if (!activeDraftSession?.session_id) return;
if (!confirm('Abandon this draft? No rewards will be granted.')) return;
try {
await api('POST', `/draft/sessions/${activeDraftSession.session_id}/abandon`);
activeDraftSession = null;
el('draft-start-btn').style.display = '';
el('draft-abandon-btn').style.display = 'none';
el('draft-status').innerHTML = '<span class="empty">Draft abandoned.</span>';
el('draft-candidates').innerHTML = '';
el('draft-picks').innerHTML = '';
showToast('Draft abandoned.');
} catch (e) { showToast(e.message, true); }
}
async function pickDraftCard(cardId) {
if (!activeDraftSession?.session_id) return;
try {
const data = await api('POST', `/draft/sessions/${activeDraftSession.session_id}/pick`, { card_id: cardId });
activeDraftSession = data;
renderDraftSession(data);
if (data.just_completed) {
const r = data.reward || {};
showToast(`Draft complete! ${(r.coins || 0).toLocaleString()} coins earned (avg OVR ${r.squad_rating || 0})`);
el('draft-abandon-btn').style.display = 'none';
api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {});
}
} catch (e) { showToast(e.message, true); }
}
function renderDraftSession(data) {
const picks = data.picks || [];
const candidates = data.candidates || [];
const progress = data.progress || {};
const status = data.status;
const isComplete = status === 'completed';
// Dots progress bar
const dots = Array.from({length: progress.total || 11}, (_, i) =>
`<span class="pick-dot ${i < progress.filled ? 'filled' : ''}"></span>`
).join('');
el('draft-status').innerHTML = `
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
<strong>${isComplete ? 'Draft Complete!' : `Picking: ${data.current_position || '—'}`}</strong>
<span style="font-size:0.8rem;color:#8b949e">${progress.filled || 0}/${progress.total || 11}</span>
</div>
<div class="pick-slots" style="margin-bottom:12px">${dots}</div>
${isComplete ? `
<div style="background:#0d2818;border-radius:6px;padding:12px;margin-bottom:12px">
<div style="font-size:0.9rem"><strong>Reward:</strong> ${(data.reward?.coins || 0).toLocaleString()} coins
${data.reward?.pack_id ? ' + ' + data.reward.pack_id : ''}
· Squad Rating: ${data.reward?.squad_rating || data.squad_rating || '—'}</div>
</div>
<button class="btn btn-primary btn-sm" onclick="startDraft()">Start Another Draft</button>` : ''}`;
// Candidates
if (!isComplete && candidates.length) {
el('draft-candidates').innerHTML = candidates.map(cand => {
const c = cand.card;
if (!c) return '';
return `<div class="draft-candidate" onclick="pickDraftCard('${cand.card_id}')">
<span class="card-rarity ${rarityClass(c.rarity)}" style="position:static;display:inline-block;margin-bottom:4px">${(c.rarity||'').toUpperCase()}</span>
<div class="card-ovr" style="font-size:1.3rem">${c.overall}</div>
<div style="font-size:0.7rem;color:#8b949e;text-transform:uppercase">${c.position}</div>
<div style="font-size:0.85rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${c.name}</div>
<div style="font-size:0.7rem;color:#8b949e">${c.club}</div>
<div style="margin-top:6px;font-size:0.7rem">
${statBar('PAC',c.pace)}${statBar('SHO',c.shooting)}${statBar('PAS',c.passing)}
</div>
</div>`;
}).join('');
} else if (isComplete) {
el('draft-candidates').innerHTML = '';
el('draft-start-btn').style.display = '';
el('draft-abandon-btn').style.display = 'none';
}
// Picks list
el('draft-picks').innerHTML = picks.map(p => {
const c = p.card;
if (!c) return `<div class="squad-slot"><div class="slot-pos">${p.position}</div><span style="color:#8b949e;font-size:0.8rem">—</span></div>`;
return `<div class="squad-slot">
<div class="slot-pos">${p.position}</div>
<div class="card-ovr" style="font-size:1.1rem">${c.overall}</div>
<div style="font-size:0.8rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${c.name}</div>
<div style="font-size:0.7rem;color:#8b949e">${c.position}</div>
</div>`;
}).join('');
}
// ── Market ────────────────────────────────────────────────────────────────────
let allMarketListings = [];
async function loadMarket() {
await Promise.all([loadMarketListings(), loadMyListings()]);
// Populate sell card dropdown from collection
if (!allCards.length) {
try { const d = await api('GET', '/collection'); allCards = d.collection || []; } catch (_) {}
}
const sel = el('sell-card-select');
sel.innerHTML = '<option value="">— choose card —</option>' +
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})</option>`;
}).join('');
}
async function loadMarketListings() {
try {
const data = await api('GET', '/market');
allMarketListings = data.listings || [];
filterMarket();
} catch (e) { el('market-grid').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
function filterMarket() {
const q = (el('m-search')?.value || '').toLowerCase();
const filtered = q
? allMarketListings.filter(l => l.card && l.card.name.toLowerCase().includes(q))
: allMarketListings;
if (!filtered.length) {
el('market-grid').innerHTML = '<span class="empty">No listings found. Click "Refresh Listings" to populate the market.</span>';
return;
}
el('market-grid').innerHTML = filtered.map(l => {
const c = l.card;
if (!c) return '';
return `<div class="market-item">
<span class="card-rarity ${rarityClass(c.rarity)}" style="position:static;display:inline-block;margin-bottom:4px">${(c.rarity||'').toUpperCase()}</span>
<div class="card-ovr" style="font-size:1.3rem">${c.overall}</div>
<div style="font-size:0.7rem;color:#8b949e;text-transform:uppercase">${c.position}</div>
<div style="font-size:0.85rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${c.name}</div>
<div style="font-size:0.7rem;color:#8b949e">${c.club} · ${c.nation}</div>
<div class="market-price">${(l.listing?.buy_now_price || l.buy_now_price || 0).toLocaleString()} coins</div>
<div class="market-seller">${l.listing?.seller_name || l.seller_name || 'NPC'}</div>
<button class="btn btn-primary btn-sm" style="margin-top:8px;width:100%"
onclick="buyListing('${l.listing?.id || l.id}', this)">Buy</button>
</div>`;
}).join('');
}
async function refreshMarket() {
try {
await api('POST', '/market/refresh');
showToast('Market refreshed!');
await loadMarketListings();
} catch (e) { showToast(e.message, true); }
}
async function buyListing(listingId, btn) {
btn.disabled = true; btn.textContent = 'Buying…';
try {
const r = await api('POST', '/market/buy', { listing_id: listingId });
showToast(`Bought ${r.card?.name || 'card'} for ${(r.price || 0).toLocaleString()} coins`);
api('GET', '/club').then(c => { el('hdr-coins').textContent = (c.coins ?? 0).toLocaleString() + ' coins'; }).catch(() => {});
allCards = []; // invalidate collection cache
await loadMarketListings();
} catch (e) { showToast(e.message, true); btn.disabled = false; btn.textContent = 'Buy'; }
}
async function sellCard() {
const ownedCardId = el('sell-card-select').value;
const price = parseInt(el('sell-price').value, 10);
if (!ownedCardId) { showToast('Select a card first', true); return; }
if (!price || price < 100) { showToast('Price must be at least 100 coins', true); return; }
try {
await api('POST', '/market/sell', { owned_card_id: ownedCardId, buy_now_price: price });
showToast('Card listed for sale!');
allCards = [];
await loadMyListings();
} catch (e) { showToast(e.message, true); }
}
async function loadMyListings() {
try {
const data = await api('GET', '/market/my-listings');
const listings = data.listings || [];
if (!listings.length) {
el('my-listings').innerHTML = '<span class="empty">No active listings.</span>';
return;
}
el('my-listings').innerHTML = listings.map(l => {
const c = l.card;
return `<div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:10px;margin-bottom:8px;display:flex;align-items:center;gap:12px">
<div>
<div style="font-size:0.85rem;font-weight:600">${c?.name || l.listing?.card_id || '—'}</div>
<div style="font-size:0.75rem;color:#8b949e">${c?.position || ''} · ${c?.club || ''}</div>
</div>
<div style="margin-left:auto;text-align:right">
<div class="market-price" style="font-size:0.9rem">${(l.listing?.buy_now_price || 0).toLocaleString()} coins</div>
<button class="btn btn-danger btn-sm" style="margin-top:4px"
onclick="cancelListing('${l.listing?.id || ''}', this)">Cancel</button>
</div>
</div>`;
}).join('');
} catch (e) { el('my-listings').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
async function cancelListing(listingId, btn) {
btn.disabled = true;
try {
await api('DELETE', `/market/listings/${listingId}`);
showToast('Listing cancelled');
allCards = [];
await loadMyListings();
} catch (e) { showToast(e.message, true); btn.disabled = false; }
}
// ── Events ────────────────────────────────────────────────────────────────────
async function loadEvents() {
try {
const data = await api('GET', '/events');
const events = data.events || [];
if (!events.length) {
el('events-grid').innerHTML = '<span class="empty">No events defined in data/events.json.</span>';
return;
}
el('events-grid').innerHTML = events.map(ev => {
const active = ev.is_active;
const effects = ev.effects || {};
const bonuses = [
effects.score_multiplier && effects.score_multiplier !== 1 ? `Score ×${effects.score_multiplier}` : '',
effects.coin_multiplier && effects.coin_multiplier !== 1 ? `Coins ×${effects.coin_multiplier}` : '',
(effects.bonus_market_cards || []).length ? `+${effects.bonus_market_cards.length} market cards` : '',
effects.pack_discount_pct ? `-${effects.pack_discount_pct}% packs` : '',
].filter(Boolean).join(' · ');
return `<div class="event-card${active ? ' active' : ''}">
<div class="event-name">${ev.name}</div>
<div class="event-desc">${ev.description || ''}</div>
${bonuses ? `<div style="font-size:0.75rem;color:#f5a623;margin-bottom:8px">${bonuses}</div>` : ''}
<div style="display:flex;align-items:center;justify-content:space-between">
<span class="event-status ${active ? 'on' : 'off'}">${active ? '● Active' : '○ Inactive'}</span>
${active
? `<button class="btn btn-secondary btn-sm" onclick="toggleEvent('${ev.id}', false, this)">Deactivate</button>`
: `<button class="btn btn-primary btn-sm" onclick="toggleEvent('${ev.id}', true, this)">Activate</button>`}
</div>
</div>`;
}).join('');
} catch (e) { el('events-grid').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
async function toggleEvent(eventId, activate, btn) {
btn.disabled = true;
try {
const path = activate ? `/events/${eventId}/activate` : `/events/${eventId}/deactivate`;
await api('POST', path);
showToast(activate ? 'Event activated!' : 'Event deactivated');
loadEvents();
} catch (e) { showToast(e.message, true); btn.disabled = false; }
}
// ── Notifications ─────────────────────────────────────────────────────────────
async function loadNotifications() {
+4
View File
@@ -240,6 +240,10 @@ async fn test_dashboard_contains_key_sections() {
assert!(html.contains("tab-objectives"), "objectives tab missing");
assert!(html.contains("tab-division"), "division tab missing");
assert!(html.contains("tab-champs"), "fut champs tab missing");
assert!(html.contains("tab-squad"), "squad tab missing");
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-notifications"), "notifications tab missing");
}