This commit is contained in:
2026-03-18 15:33:24 +01:00
parent 5e7a6808ab
commit 867c51062b
39 changed files with 6499 additions and 161 deletions
+344
View File
@@ -0,0 +1,344 @@
<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';
let profile = $state(null);
let loading = $state(true);
const token = () => localStorage.getItem('token');
onMount(async () => {
if (!token()) { goto('/auth'); return; }
const res = await apiFetch(`${API_URL}/profile`);
if (res.status === 401) { goto('/auth'); return; }
profile = await res.json();
loading = false;
});
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('refresh_token');
goto('/auth');
}
</script>
<main>
{#if loading}
<p class="status">Loading...</p>
{:else if profile}
<div class="profile">
<div class="profile-header">
<div class="avatar">{profile.username[0].toUpperCase()}</div>
<div class="profile-info">
<h1 class="username">{profile.username}</h1>
<p class="email">{profile.email}</p>
<p class="joined">Member since {new Date(profile.created_at).toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
</div>
<button class="logout-btn" onclick={logout}>Log Out</button>
<a href="/reset-password" class="reset-link">Change password</a>
</div>
<div class="section-divider"></div>
<h2 class="section-title">Stats</h2>
<div class="stats-grid">
<div class="stat-card">
<span class="stat-label">Wins</span>
<span class="stat-value wins">{profile.wins}</span>
</div>
<div class="stat-card">
<span class="stat-label">Losses</span>
<span class="stat-value losses">{profile.losses}</span>
</div>
<div class="stat-card">
<span class="stat-label">Games Played</span>
<span class="stat-value">{profile.wins + profile.losses}</span>
</div>
<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}%` : '—'}
</span>
</div>
</div>
<div class="section-divider"></div>
<h2 class="section-title">Highlights</h2>
<div class="highlights">
<div class="highlight-card">
<span class="highlight-label">Most Played Deck</span>
{#if profile.most_played_deck}
<span class="highlight-value">{profile.most_played_deck.name}</span>
<span class="highlight-sub">{profile.most_played_deck.times_played} games</span>
{:else}
<span class="no-data">No games played yet</span>
{/if}
</div>
<div class="highlight-card">
<span class="highlight-label">Most Played Card</span>
{#if profile.most_played_card}
<div class="card-preview">
{#if profile.most_played_card.image_link}
<img src={profile.most_played_card.image_link} alt={profile.most_played_card.name} class="card-thumb" />
{/if}
<div class="card-preview-info">
<span class="highlight-value">{profile.most_played_card.name}</span>
<span class="highlight-sub">{profile.most_played_card.times_played} times played</span>
<span class="highlight-sub">{profile.most_played_card.card_type} · {profile.most_played_card.card_rarity}</span>
</div>
</div>
{:else}
<span class="no-data">No cards played yet</span>
{/if}
</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;
}
.profile {
max-width: 700px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.profile-header {
display: flex;
align-items: center;
gap: 1.5rem;
}
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
background: #3d2507;
border: 2px solid #c8861a;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Cinzel', serif;
font-size: 28px;
font-weight: 700;
color: #f0d080;
flex-shrink: 0;
}
.profile-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.username {
font-family: 'Cinzel', serif;
font-size: 24px;
font-weight: 700;
color: #f0d080;
margin: 0;
}
.email {
font-family: 'Crimson Text', serif;
font-size: 15px;
color: rgba(240, 180, 80, 0.5);
margin: 0;
}
.joined {
font-family: 'Crimson Text', serif;
font-size: 13px;
font-style: italic;
color: rgba(240, 180, 80, 0.35);
margin: 0;
}
.logout-btn {
font-family: 'Cinzel', serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: none;
border: 1px solid rgba(180, 60, 60, 0.4);
border-radius: 4px;
color: rgba(200, 80, 80, 0.7);
padding: 8px 16px;
cursor: pointer;
transition: all 0.15s;
align-self: flex-start;
}
.logout-btn:hover {
border-color: #c84040;
color: #e05050;
background: rgba(180, 40, 40, 0.1);
}
.reset-link {
font-family: 'Crimson Text', serif;
font-size: 14px;
font-style: italic;
color: rgba(240, 180, 80, 0.4);
text-decoration: underline;
align-self: flex-start;
transition: color 0.15s;
}
.reset-link:hover { color: rgba(240, 180, 80, 0.7); }
.section-divider {
height: 1px;
background: rgba(107, 76, 30, 0.3);
}
.section-title {
font-family: 'Cinzel', serif;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(240, 180, 80, 0.4);
margin: 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.stat-card {
background: #1a1008;
border: 1px solid rgba(107, 76, 30, 0.3);
border-radius: 8px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.stat-label {
font-family: 'Cinzel', serif;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(240, 180, 80, 0.4);
}
.stat-value {
font-family: 'Cinzel', serif;
font-size: 28px;
font-weight: 700;
color: #f0d080;
}
.wins { color: #6aaa6a; }
.losses { color: #c85050; }
.good-wr { color: #6aaa6a; }
.bad-wr { color: #c85050; }
.highlights {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.highlight-card {
background: #1a1008;
border: 1px solid rgba(107, 76, 30, 0.3);
border-radius: 8px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.highlight-label {
font-family: 'Cinzel', serif;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(240, 180, 80, 0.4);
}
.highlight-value {
font-family: 'Cinzel', serif;
font-size: 16px;
font-weight: 700;
color: #f0d080;
}
.highlight-sub {
font-family: 'Crimson Text', serif;
font-size: 13px;
font-style: italic;
color: rgba(240, 180, 80, 0.45);
}
.card-preview {
display: flex;
gap: 0.75rem;
align-items: flex-start;
margin-top: 0.25rem;
}
.card-thumb {
width: 48px;
height: 48px;
object-fit: cover;
object-position: top;
border-radius: 4px;
border: 1px solid rgba(107, 76, 30, 0.4);
flex-shrink: 0;
}
.card-preview-info {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.no-data {
font-family: 'Crimson Text', serif;
font-size: 14px;
font-style: italic;
color: rgba(240, 180, 80, 0.25);
}
.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;
}
@media (max-width: 640px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.highlights { grid-template-columns: 1fr; }
.profile-header { flex-wrap: wrap; }
}
</style>