🐐
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
{/if}
|
||||
|
||||
<svelte:head>
|
||||
<title>WikiTCG</title>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
let sortAsc = $state(true);
|
||||
let costMin = $state(1);
|
||||
let costMax = $state(12);
|
||||
let costMax = $state(11);
|
||||
|
||||
let filtered = $derived.by(() => {
|
||||
let result = allCards.filter(c =>
|
||||
@@ -236,14 +236,14 @@
|
||||
<div class="filter-group">
|
||||
<div class="filter-group-header">
|
||||
<span class="filter-group-label">Cost</span>
|
||||
<button class="select-all" onclick={() => { costMin = 1; costMax = 12; }}>Reset</button>
|
||||
<button class="select-all" onclick={() => { costMin = 1; costMax = 11; }}>Reset</button>
|
||||
</div>
|
||||
<div class="cost-range">
|
||||
<span class="range-label">Min: {costMin}</span>
|
||||
<input type="range" min="1" max="12" bind:value={costMin}
|
||||
<input type="range" min="1" max="11" bind:value={costMin}
|
||||
oninput={() => { if (costMin > costMax) costMax = costMin; }} />
|
||||
<span class="range-label">Max: {costMax}</span>
|
||||
<input type="range" min="1" max="12" bind:value={costMax}
|
||||
<input type="range" min="1" max="11" bind:value={costMax}
|
||||
oninput={() => { if (costMax < costMin) costMin = costMax; }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Cards</th>
|
||||
<th>Cost</th>
|
||||
<th>Type</th>
|
||||
<th>Played</th>
|
||||
<th>W / L</th>
|
||||
@@ -87,8 +88,9 @@
|
||||
{@const wr = winRate(deck)}
|
||||
<tr>
|
||||
<td class="deck-name">{deck.name}</td>
|
||||
<td class="deck-count" class:incomplete={deck.card_count < 20}>
|
||||
{deck.card_count}/20
|
||||
<td class="deck-count">{deck.card_count}</td>
|
||||
<td class="deck-cost" class:over-budget={deck.total_cost > 50}>
|
||||
{deck.total_cost}/50
|
||||
</td>
|
||||
<td class="deck-type">
|
||||
<DeckTypeBadge deckType={deck.deck_type} />
|
||||
@@ -100,14 +102,14 @@
|
||||
<span class="separator"> / </span>
|
||||
<span class="losses">{deck.losses}</span>
|
||||
{:else}
|
||||
<span class="no-data">—</span>
|
||||
<span class="no-data">-</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="deck-stat">
|
||||
{#if wr !== null}
|
||||
<span class:good-wr={wr >= 50} class:bad-wr={wr < 50}>{wr}%</span>
|
||||
{:else}
|
||||
<span class="no-data">—</span>
|
||||
<span class="no-data">-</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="deck-actions">
|
||||
@@ -144,7 +146,7 @@
|
||||
<p class="popup-body">
|
||||
Are you sure you want to delete <strong>{deleteConfirm.name}</strong>?
|
||||
{#if deleteConfirm.times_played > 0}
|
||||
This deck has been played {deleteConfirm.times_played} time{deleteConfirm.times_played === 1 ? '' : 's'} — its stats will be preserved in your profile history.
|
||||
This deck has been played {deleteConfirm.times_played} time{deleteConfirm.times_played === 1 ? '' : 's'}. Its stats will be preserved in your profile history.
|
||||
{/if}
|
||||
</p>
|
||||
<div class="popup-actions">
|
||||
@@ -240,6 +242,14 @@
|
||||
}
|
||||
|
||||
.deck-count {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(240, 180, 80, 0.6);
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.deck-cost {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
@@ -247,7 +257,7 @@
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.deck-count.incomplete { color: #c85050; }
|
||||
.deck-cost.over-budget { color: #c85050; }
|
||||
|
||||
.deck-type { width: 90px; }
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
let selectedRarities = $state(new Set(RARITIES));
|
||||
let selectedTypes = $state(new Set(TYPES));
|
||||
let costMin = $state(1);
|
||||
let costMax = $state(12);
|
||||
let costMax = $state(11);
|
||||
let filtersOpen = $state(false);
|
||||
|
||||
function label(str) {
|
||||
@@ -74,12 +74,17 @@
|
||||
function toggleAllRarities() { selectedRarities = allRaritiesSelected() ? new Set() : new Set(RARITIES); }
|
||||
function toggleAllTypes() { selectedTypes = allTypesSelected() ? new Set() : new Set(TYPES); }
|
||||
|
||||
const selectedCost = $derived(
|
||||
allCards.filter(c => selectedIds.has(c.id)).reduce((sum, c) => sum + c.cost, 0)
|
||||
);
|
||||
|
||||
function toggleCard(id) {
|
||||
const s = new Set(selectedIds);
|
||||
if (s.has(id)) {
|
||||
s.delete(id);
|
||||
} else {
|
||||
if (s.size >= 20) return;
|
||||
const card = allCards.find(c => c.id === id);
|
||||
if (card && selectedCost + card.cost > 50) return;
|
||||
s.add(id);
|
||||
}
|
||||
selectedIds = s;
|
||||
@@ -151,8 +156,8 @@
|
||||
{/if}
|
||||
|
||||
<div class="header-right">
|
||||
<span class="card-counter" class:full={selectedIds.size === 20} class:empty={selectedIds.size === 0}>
|
||||
{selectedIds.size}/20
|
||||
<span class="card-counter" class:full={selectedCost === 50} class:over={selectedCost > 50} class:empty={selectedIds.size === 0}>
|
||||
{selectedIds.size} cards · {selectedCost}/50
|
||||
</span>
|
||||
<button class="done-btn" onclick={save} disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Done'}
|
||||
@@ -171,7 +176,7 @@
|
||||
|
||||
<button class="filter-toggle" onclick={() => filtersOpen = !filtersOpen}>
|
||||
{filtersOpen ? 'Hide filters' : 'Filter'}
|
||||
{#if selectedRarities.size < RARITIES.length || selectedTypes.size < TYPES.length || costMin > 1 || costMax < 12}
|
||||
{#if selectedRarities.size < RARITIES.length || selectedTypes.size < TYPES.length || costMin > 1 || costMax < 11}
|
||||
<span class="filter-dot"></span>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -212,13 +217,13 @@
|
||||
<div class="filter-group">
|
||||
<div class="filter-group-header">
|
||||
<span class="filter-group-label">Cost</span>
|
||||
<button class="select-all" onclick={() => { costMin = 1; costMax = 12; }}>Reset</button>
|
||||
<button class="select-all" onclick={() => { costMin = 1; costMax = 11; }}>Reset</button>
|
||||
</div>
|
||||
<div class="cost-range">
|
||||
<span class="range-label">Min: {costMin}</span>
|
||||
<input type="range" min="1" max="12" bind:value={costMin} oninput={() => { if (costMin > costMax) costMax = costMin; }} />
|
||||
<input type="range" min="1" max="11" bind:value={costMin} oninput={() => { if (costMin > costMax) costMax = costMin; }} />
|
||||
<span class="range-label">Max: {costMax}</span>
|
||||
<input type="range" min="1" max="12" bind:value={costMax} oninput={() => { if (costMax < costMin) costMin = costMax; }} />
|
||||
<input type="range" min="1" max="11" bind:value={costMax} oninput={() => { if (costMax < costMin) costMin = costMax; }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,7 +240,7 @@
|
||||
<button
|
||||
class="card-wrap"
|
||||
class:selected={selectedIds.has(card.id)}
|
||||
class:disabled={!selectedIds.has(card.id) && selectedIds.size >= 20}
|
||||
class:disabled={!selectedIds.has(card.id) && selectedCost + card.cost > 50}
|
||||
onclick={() => toggleCard(card.id)}
|
||||
>
|
||||
<Card {card} noHover={true} />
|
||||
@@ -317,11 +322,12 @@
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #c85050;
|
||||
color: rgba(240, 180, 80, 0.7);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.card-counter.full { color: #6aaa6a; }
|
||||
.card-counter.full { color: #6aaa6a; }
|
||||
.card-counter.over { color: #c85050; }
|
||||
.card-counter.empty { color: rgba(240, 180, 80, 0.3); }
|
||||
|
||||
.done-btn {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
const annotations = [
|
||||
{ number: 1, label: "Name", description: "The name of the Wikipedia article this card was generated from." },
|
||||
{ number: 2, label: "Type", description: "The category of the subject — Person, Location, Artwork, etc." },
|
||||
{ number: 2, label: "Type", description: "The category of the subject. Person, Location, Artwork, etc." },
|
||||
{ number: 3, label: "Rarity badge", description: "Rarity is determined by the article's WikiRank quality score. From lowest to highest: Common, Uncommon, Rare, Super Rare, Epic, and Legendary." },
|
||||
{ number: 4, label: "Wikipedia link", description: "Opens the Wikipedia article this card was generated from." },
|
||||
{ number: 5, label: "Cost bubbles", description: "How much energy it costs to play this card. Derived from the card's attack and defense stats." },
|
||||
@@ -29,14 +29,14 @@
|
||||
|
||||
// Annotation positions as percentage of card width/height
|
||||
const markerPositions = [
|
||||
{ number: 1, x: 15, y: 3 }, // name — top center
|
||||
{ number: 2, x: 75, y: 3 }, // type badge — top right
|
||||
{ number: 3, x: 14, y: 20 }, // rarity badge — top left of image
|
||||
{ number: 4, x: 85, y: 20 }, // wiki link — top right of image
|
||||
{ number: 5, x: 15, y: 53 }, // cost bubbles — bottom left of image
|
||||
{ number: 6, x: 50, y: 73 }, // text — middle
|
||||
{ number: 7, x: 15, y: 88 }, // attack — bottom left
|
||||
{ number: 8, x: 85, y: 88 }, // defense — bottom right
|
||||
{ number: 1, x: 15, y: 3 }, // name. top center
|
||||
{ number: 2, x: 75, y: 3 }, // type badge. top right
|
||||
{ number: 3, x: 14, y: 20 }, // rarity badge. top left of image
|
||||
{ number: 4, x: 85, y: 20 }, // wiki link. top right of image
|
||||
{ number: 5, x: 15, y: 53 }, // cost bubbles. bottom left of image
|
||||
{ number: 6, x: 50, y: 73 }, // text. middle
|
||||
{ number: 7, x: 15, y: 88 }, // attack. bottom left
|
||||
{ number: 8, x: 85, y: 88 }, // defense. bottom right
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -102,6 +102,22 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2 class="section-title">Building a Deck</h2>
|
||||
<div class="rules-grid">
|
||||
<div class="rule-card">
|
||||
<div class="rule-icon">✦</div>
|
||||
<h3 class="rule-title">Cost Limit</h3>
|
||||
<p class="rule-body">Your deck's total cost, the sum of all card costs, must be 50 or less. You can't queue for a game with an empty deck or one that exceeds the limit.</p>
|
||||
</div>
|
||||
<div class="rule-card">
|
||||
<div class="rule-icon">🃏</div>
|
||||
<h3 class="rule-title">No Card Limit</h3>
|
||||
<p class="rule-body">There is no minimum or maximum number of cards. On the extreme ends, you can have just 4 11-cost cards, or 50 1-cost cards.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2 class="section-title">Taking a Turn</h2>
|
||||
<div class="rules-grid">
|
||||
@@ -123,7 +139,7 @@
|
||||
<div class="rule-card">
|
||||
<div class="rule-icon">🗡</div>
|
||||
<h3 class="rule-title">Sacrificing</h3>
|
||||
<p class="rule-body">Click the dagger icon to enter sacrifice mode, then click one of your cards to remove it from play and recover its energy cost. Use this to afford expensive cards.</p>
|
||||
<p class="rule-body">Click the dagger icon to enter sacrifice mode, then click one of your cards to remove it from play and recover 1 energy. Use this to afford expensive cards.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -21,6 +21,19 @@
|
||||
let gameState = $state(null);
|
||||
let myId = $state('');
|
||||
|
||||
let viewingBoard = $state(false);
|
||||
let showDifficultyModal = $state(false);
|
||||
let selectedDifficulty = $state(5);
|
||||
|
||||
const difficultyLabel = $derived(
|
||||
selectedDifficulty <= 2 ? 'Throws the game' :
|
||||
selectedDifficulty === 3 ? 'Fully random' :
|
||||
selectedDifficulty <= 5 ? 'Beginner' :
|
||||
selectedDifficulty <= 7 ? 'Intermediate' :
|
||||
selectedDifficulty <= 9 ? 'Advanced' :
|
||||
'Expert'
|
||||
);
|
||||
|
||||
let selectedHandIndex = $state(null);
|
||||
let combatAnimating = $state(false);
|
||||
let lunging = $state(new Set());
|
||||
@@ -69,7 +82,7 @@
|
||||
method: 'POST'
|
||||
});
|
||||
if (!res.ok) {
|
||||
// Server rejected the claim — game may have ended another way
|
||||
// Server rejected the claim. Game may have ended another way
|
||||
const err = await res.json();
|
||||
console.warn('Timeout claim rejected:', err.detail);
|
||||
}
|
||||
@@ -78,8 +91,8 @@
|
||||
$effect(() => {
|
||||
if (!gameState || combatAnimating) return;
|
||||
displayedLife = {
|
||||
[gameState.you.user_id]: gameState.you.life,
|
||||
[gameState.opponent.user_id]: gameState.opponent.life,
|
||||
[gameState.you.user_id]: Math.max(0, gameState.you.life),
|
||||
[gameState.opponent.user_id]: Math.max(0, gameState.opponent.life),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -113,7 +126,7 @@
|
||||
});
|
||||
|
||||
function joinQueue() {
|
||||
if (!selectedDeckId || selectedDeck?.card_count < 20) return;
|
||||
if (!selectedDeckId || selectedDeck?.total_cost === 0 || selectedDeck?.total_cost > 50) return;
|
||||
error = '';
|
||||
phase = 'queuing';
|
||||
queueWs = new WebSocket(`${WS_URL}/ws/queue?deck_id=${selectedDeckId}`);
|
||||
@@ -168,7 +181,7 @@
|
||||
combatAnimating = true;
|
||||
|
||||
// The attacker is whoever was active when end_turn was called.
|
||||
// After end_turn resolves, active_player_id switches — so we look
|
||||
// After end_turn resolves, active_player_id switches. So we look
|
||||
// at who is NOT the current active player to find the attacker,
|
||||
// unless the game just ended (result is set), in which case
|
||||
// active_player_id hasn't switched yet.
|
||||
@@ -267,10 +280,11 @@
|
||||
}
|
||||
|
||||
async function joinSolo() {
|
||||
if (!selectedDeckId || selectedDeck?.card_count < 20) return;
|
||||
if (!selectedDeckId || selectedDeck?.total_cost === 0 || selectedDeck?.total_cost > 50) return;
|
||||
showDifficultyModal = false;
|
||||
error = '';
|
||||
phase = 'queuing';
|
||||
const res = await apiFetch(`${API_URL}/game/solo?deck_id=${selectedDeckId}&difficulty=5`, {
|
||||
const res = await apiFetch(`${API_URL}/game/solo?deck_id=${selectedDeckId}&difficulty=${selectedDifficulty}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -297,16 +311,16 @@
|
||||
<label class="deck-label" for="deck">Choose your deck</label>
|
||||
<select id="deck" bind:value={selectedDeckId}>
|
||||
{#each decks as deck}
|
||||
<option value={deck.id}>{deck.name} ({deck.card_count}/20)</option>
|
||||
<option value={deck.id}>{deck.name} ({deck.card_count} cards, cost {deck.total_cost})</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<p class="error">{selectedDeck && selectedDeck.card_count < 20 ? `Deck must have 20 cards (${selectedDeck.card_count}/20)` : ''}</p>
|
||||
<p class="error">{selectedDeck && (selectedDeck.total_cost === 0 || selectedDeck.total_cost > 50) ? `Deck cost must be between 1 and 50 (current: ${selectedDeck.total_cost})` : ''}</p>
|
||||
<div class="lobby-buttons">
|
||||
<button class="play-btn" onclick={joinQueue} disabled={!selectedDeckId || selectedDeck?.card_count < 20}>
|
||||
<button class="play-btn" onclick={joinQueue} disabled={!selectedDeckId || selectedDeck?.total_cost === 0 || selectedDeck?.total_cost > 50}>
|
||||
Find Opponent
|
||||
</button>
|
||||
<button class="play-btn solo-btn" onclick={joinSolo} disabled={!selectedDeckId || selectedDeck?.card_count < 20}>
|
||||
<button class="play-btn solo-btn" onclick={() => showDifficultyModal = true} disabled={!selectedDeckId || selectedDeck?.total_cost === 0 || selectedDeck?.total_cost > 50}>
|
||||
vs. Computer
|
||||
</button>
|
||||
</div>
|
||||
@@ -320,7 +334,7 @@
|
||||
<button class="cancel-btn" onclick={() => { queueWs?.close(); phase = 'idle'; }}>Cancel</button>
|
||||
</div>
|
||||
|
||||
{:else if phase === 'playing' && gameState}
|
||||
{:else if (phase === 'playing' || (phase === 'ended' && viewingBoard)) && gameState}
|
||||
<div class="game">
|
||||
|
||||
<div class="sidebar left-sidebar">
|
||||
@@ -374,7 +388,7 @@
|
||||
|
||||
<div class="divider">
|
||||
<span class="turn-indicator" class:my-turn={isMyTurn}>
|
||||
{isMyTurn ? 'Your turn' : `${opp.username}'s turn`}
|
||||
{phase === 'ended' ? 'Game Ended' : isMyTurn ? 'Your turn' : `${opp.username}'s turn`}
|
||||
</span>
|
||||
{#if secondsRemaining <= TIMER_WARNING}
|
||||
<span class="turn-timer" class:urgent={secondsRemaining <= 10}>
|
||||
@@ -420,7 +434,9 @@
|
||||
</div>
|
||||
|
||||
<div class="sidebar right-sidebar">
|
||||
{#if isMyTurn && !combatAnimating}
|
||||
{#if phase === 'ended'}
|
||||
<button class="end-turn-btn" onclick={() => viewingBoard = false}>Go Back</button>
|
||||
{:else if isMyTurn && !combatAnimating}
|
||||
<button class="end-turn-btn" onclick={endTurn}>End Turn</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -453,7 +469,36 @@
|
||||
{gameState.result.winner_id === myId ? 'Victory' : 'Defeat'}
|
||||
</h1>
|
||||
<p class="lobby-hint">{gameState.result.reason}</p>
|
||||
<button class="play-btn" onclick={() => { gameState = null; phase = 'idle'; }}>Go back</button>
|
||||
<div class="lobby-buttons">
|
||||
<button class="play-btn" onclick={() => viewingBoard = true}>View Board</button>
|
||||
<button class="play-btn" onclick={() => { gameState = null; phase = 'idle'; }}>Go Back</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showDifficultyModal}
|
||||
<div class="modal-backdrop" onclick={() => showDifficultyModal = false}>
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()}>
|
||||
<h2 class="modal-title">Choose Difficulty</h2>
|
||||
<div class="difficulty-display">
|
||||
<span class="difficulty-number">{selectedDifficulty}</span>
|
||||
<span class="difficulty-label">{difficultyLabel}</span>
|
||||
</div>
|
||||
<input
|
||||
class="difficulty-slider"
|
||||
type="range"
|
||||
min="1"
|
||||
max="10"
|
||||
bind:value={selectedDifficulty}
|
||||
/>
|
||||
<div class="difficulty-ticks">
|
||||
<span>1</span><span>10</span>
|
||||
</div>
|
||||
<div class="modal-buttons">
|
||||
<button class="cancel-btn" onclick={() => showDifficultyModal = false}>Cancel</button>
|
||||
<button class="play-btn" onclick={joinSolo}>Start Game</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1040,6 +1085,82 @@
|
||||
.hand-card.unaffordable { filter: grayscale(0.7) brightness(0.55); }
|
||||
.hand-card:disabled { cursor: default; }
|
||||
|
||||
/* ── Difficulty modal ── */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #110d04;
|
||||
border: 1.5px solid #6b4c1e;
|
||||
border-radius: 10px;
|
||||
padding: 2rem 2.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #f0d080;
|
||||
margin: 0;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.difficulty-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.difficulty-number {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
color: #f0d080;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.difficulty-label {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 16px;
|
||||
font-style: italic;
|
||||
color: rgba(240, 180, 80, 0.6);
|
||||
}
|
||||
|
||||
.difficulty-slider {
|
||||
width: 100%;
|
||||
accent-color: #c8861a;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.difficulty-ticks {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 10px;
|
||||
color: rgba(240, 180, 80, 0.4);
|
||||
margin-top: -0.75rem;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* ── Toast ── */
|
||||
.toast {
|
||||
position: fixed;
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Win Rate</span>
|
||||
<span class="stat-value" class:good-wr={profile.win_rate >= 50} class:bad-wr={profile.win_rate !== null && profile.win_rate < 50}>
|
||||
{profile.win_rate !== null ? `${profile.win_rate}%` : '—'}
|
||||
{profile.win_rate !== null ? `${profile.win_rate}%` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user