Files
wiki-tcg/frontend/src/routes/decks/+page.svelte
2026-03-18 15:33:24 +01:00

441 lines
11 KiB
Svelte

<script>
import { API_URL, WS_URL } from '$lib/api.js';
import { apiFetch } from '$lib/api.js';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import DeckTypeBadge from '$lib/DeckTypeBadge.svelte';
let decks = $state([]);
let loading = $state(true);
let editConfirm = $state(null); // deck object pending edit confirmation
let deleteConfirm = $state(null); // deck object pending delete confirmation
const token = () => localStorage.getItem('token');
onMount(async () => {
if (!token()) { goto('/auth'); return; }
await fetchDecks();
});
async function fetchDecks() {
const res = await apiFetch(`${API_URL}/decks`);
if (res.status === 401) { goto('/auth'); return; }
decks = await res.json();
loading = false;
}
async function createDeck() {
const res = await apiFetch(`${API_URL}/decks`, {
method: 'POST'
});
const deck = await res.json();
goto(`/decks/${deck.id}`);
}
function clickEdit(deck) {
if (deck.times_played > 0) {
editConfirm = deck;
} else {
goto(`/decks/${deck.id}`);
}
}
function clickDelete(deck) {
deleteConfirm = deck;
}
async function confirmDelete() {
await apiFetch(`${API_URL}/decks/${deleteConfirm.id}`, {
method: 'DELETE'
});
decks = decks.filter(d => d.id !== deleteConfirm.id);
deleteConfirm = null;
}
function winRate(deck) {
if (deck.times_played === 0) return null;
return Math.round((deck.wins / deck.times_played) * 100);
}
</script>
<main>
<div class="header">
<h1 class="title">Your Decks</h1>
<button class="new-btn" onclick={createDeck}>+ New Deck</button>
</div>
{#if loading}
<p class="status">Loading decks...</p>
{:else if decks.length === 0}
<p class="status">You have no decks yet.</p>
{:else}
<table>
<thead>
<tr>
<th>Name</th>
<th>Cards</th>
<th>Type</th>
<th>Played</th>
<th>W / L</th>
<th>Win %</th>
<th></th>
</tr>
</thead>
<tbody>
{#each decks as deck}
{@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>
<td class="deck-type">
<DeckTypeBadge deckType={deck.deck_type} />
</td>
<td class="deck-stat">{deck.times_played}</td>
<td class="deck-stat">
{#if deck.times_played > 0}
<span class="wins">{deck.wins}</span>
<span class="separator"> / </span>
<span class="losses">{deck.losses}</span>
{:else}
<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>
{/if}
</td>
<td class="deck-actions">
<button class="edit-btn" onclick={() => clickEdit(deck)}>Edit</button>
<button class="delete-btn" onclick={() => clickDelete(deck)}>Delete</button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
<!-- Edit confirmation popup -->
{#if editConfirm}
<div class="backdrop" onclick={() => editConfirm = null}>
<div class="popup" onclick={(e) => e.stopPropagation()}>
<h2 class="popup-title">Reset Stats?</h2>
<p class="popup-body">
<strong>{editConfirm.name}</strong> has been played {editConfirm.times_played} time{editConfirm.times_played === 1 ? '' : 's'} ({editConfirm.wins}W / {editConfirm.losses}L). Editing this deck will reset its stats.
</p>
<div class="popup-actions">
<button class="popup-cancel" onclick={() => editConfirm = null}>Cancel</button>
<button class="popup-confirm" onclick={() => { goto(`/decks/${editConfirm.id}`); editConfirm = null; }}>Continue</button>
</div>
</div>
</div>
{/if}
<!-- Delete confirmation popup -->
{#if deleteConfirm}
<div class="backdrop" onclick={() => deleteConfirm = null}>
<div class="popup" onclick={(e) => e.stopPropagation()}>
<h2 class="popup-title">Delete Deck?</h2>
<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.
{/if}
</p>
<div class="popup-actions">
<button class="popup-cancel" onclick={() => deleteConfirm = null}>Cancel</button>
<button class="popup-delete" onclick={confirmDelete}>Delete</button>
</div>
</div>
</div>
{/if}
</main>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap');
main {
height: 100vh;
overflow-y: auto;
background: #0d0a04;
padding: 2rem;
}
.header {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 2rem;
border-bottom: 1px solid rgba(107, 76, 30, 0.3);
padding-bottom: 1rem;
}
.title {
font-family: 'Cinzel', serif;
font-size: 22px;
font-weight: 700;
color: #f0d080;
margin: 0;
}
.new-btn {
font-family: 'Cinzel', serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: #3d2507;
border: 1px solid #c8861a;
border-radius: 4px;
color: #f0d080;
padding: 6px 14px;
cursor: pointer;
transition: background 0.15s;
}
.new-btn:hover { background: #5a3510; }
table {
width: 100%;
border-collapse: collapse;
font-family: 'Crimson Text', serif;
}
thead tr {
border-bottom: 1px solid rgba(107, 76, 30, 0.5);
}
th {
font-family: 'Cinzel', serif;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(240, 180, 80, 0.4);
padding: 0 1rem 0.75rem 0;
text-align: left;
}
tbody tr {
border-bottom: 1px solid rgba(107, 76, 30, 0.2);
transition: background 0.1s;
}
tbody tr:hover { background: rgba(107, 76, 30, 0.08); }
td {
padding: 0.9rem 1rem 0.9rem 0;
vertical-align: middle;
}
.deck-name {
font-size: 17px;
color: #e8d090;
}
.deck-count {
font-family: 'Cinzel', serif;
font-size: 13px;
font-weight: 700;
color: #6aaa6a;
width: 60px;
}
.deck-count.incomplete { color: #c85050; }
.deck-type { width: 90px; }
.type-badge {
font-family: 'Cinzel', serif;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.05em;
padding: 3px 8px;
border-radius: 3px;
cursor: default;
}
.type-wall { background: rgba(58, 104, 120, 0.3); color: #a0d4e8; border: 1px solid rgba(58, 104, 120, 0.5); }
.type-aggro { background: rgba(139, 32, 32, 0.3); color: #e89090; border: 1px solid rgba(139, 32, 32, 0.5); }
.type-god-card { background: rgba(184, 120, 32, 0.3); color: #f5d880; border: 1px solid rgba(184, 120, 32, 0.5); }
.type-rush { background: rgba(74, 122, 80, 0.3); color: #a8dca8; border: 1px solid rgba(74, 122, 80, 0.5); }
.type-control { background: rgba(122, 80, 144, 0.3); color: #d0a0e8; border: 1px solid rgba(122, 80, 144, 0.5); }
.type-unplayable { background: rgba(60, 60, 60, 0.3); color: #909090; border: 1px solid rgba(60, 60, 60, 0.5); }
.type-pantheon { background: rgba(184, 150, 60, 0.3); color: #fce8a0; border: 1px solid rgba(184, 150, 60, 0.5); }
.type-balanced { background: rgba(106, 104, 96, 0.3); color: #c8c6c0; border: 1px solid rgba(106, 104, 96, 0.5); }
.deck-stat {
font-family: 'Cinzel', serif;
font-size: 13px;
color: rgba(240, 180, 80, 0.6);
width: 60px;
}
.wins { color: #6aaa6a; }
.losses { color: #c85050; }
.separator { color: rgba(240, 180, 80, 0.3); }
.good-wr { color: #6aaa6a; }
.bad-wr { color: #c85050; }
.no-data {
color: rgba(240, 180, 80, 0.2);
}
.deck-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.edit-btn, .delete-btn {
font-family: 'Cinzel', serif;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
border-radius: 4px;
padding: 4px 10px;
cursor: pointer;
transition: background 0.15s;
}
.edit-btn {
background: none;
border: 1px solid rgba(107, 76, 30, 0.5);
color: rgba(240, 180, 80, 0.7);
}
.edit-btn:hover {
border-color: #c8861a;
color: #f0d080;
}
.delete-btn {
background: none;
border: 1px solid rgba(180, 60, 60, 0.3);
color: rgba(200, 80, 80, 0.6);
}
.delete-btn:hover {
border-color: #c84040;
color: #e05050;
}
.status {
font-family: 'Crimson Text', serif;
font-size: 16px;
font-style: italic;
color: rgba(240, 180, 80, 0.5);
text-align: center;
margin-top: 4rem;
}
.backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.popup {
background: #1a1008;
border: 1px solid #6b4c1e;
border-radius: 10px;
padding: 2rem;
max-width: 400px;
width: calc(100% - 2rem);
display: flex;
flex-direction: column;
gap: 1rem;
}
.popup-title {
font-family: 'Cinzel', serif;
font-size: 18px;
font-weight: 700;
color: #f0d080;
margin: 0;
}
.popup-body {
font-family: 'Crimson Text', serif;
font-size: 15px;
color: rgba(240, 180, 80, 0.7);
margin: 0;
line-height: 1.6;
}
.popup-body strong {
color: #f0d080;
}
.popup-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
.popup-cancel {
font-family: 'Cinzel', serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: none;
border: 1px solid rgba(107, 76, 30, 0.4);
border-radius: 4px;
color: rgba(240, 180, 80, 0.6);
padding: 7px 16px;
cursor: pointer;
transition: all 0.15s;
}
.popup-cancel:hover {
border-color: #c8861a;
color: #f0d080;
}
.popup-confirm {
font-family: 'Cinzel', serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: #c8861a;
border: none;
border-radius: 4px;
color: #fff8e0;
padding: 7px 16px;
cursor: pointer;
transition: background 0.15s;
}
.popup-confirm:hover { background: #e09820; }
.popup-delete {
font-family: 'Cinzel', serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: rgba(180, 40, 40, 0.8);
border: none;
border-radius: 4px;
color: #fff;
padding: 7px 16px;
cursor: pointer;
transition: background 0.15s;
}
.popup-delete:hover { background: rgba(220, 60, 60, 0.9); }
</style>