🐐
This commit is contained in:
441
frontend/src/routes/decks/+page.svelte
Normal file
441
frontend/src/routes/decks/+page.svelte
Normal file
@@ -0,0 +1,441 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user