🐐
This commit is contained in:
@@ -1,11 +1,25 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
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 profile: any = $state(null);
|
||||
let loading = $state(true);
|
||||
let wishlistText = $state('');
|
||||
let wishlistSaving = $state(false);
|
||||
let wishlistSaved = $state(false);
|
||||
let wishlistError = $state('');
|
||||
|
||||
let friends: any[] = $state([]);
|
||||
let proposals: any[] = $state([]);
|
||||
let challenges: any[] = $state([]);
|
||||
|
||||
let confirmingRemove: Set<string> = $state(new Set());
|
||||
let confirmingWithdraw: Set<string> = $state(new Set());
|
||||
|
||||
// Increments every second — passed to formatChallengeExpiry to force re-evaluation
|
||||
let tick = $state(0);
|
||||
|
||||
const token = () => localStorage.getItem('token');
|
||||
|
||||
@@ -14,9 +28,81 @@
|
||||
const res = await apiFetch(`${API_URL}/profile`);
|
||||
if (res.status === 401) { goto('/auth'); return; }
|
||||
profile = await res.json();
|
||||
wishlistText = profile.trade_wishlist || '';
|
||||
|
||||
const friendsRes = await apiFetch(`${API_URL}/friends`);
|
||||
if (friendsRes.ok) friends = await friendsRes.json();
|
||||
|
||||
const proposalsRes = await apiFetch(`${API_URL}/trade-proposals`);
|
||||
if (proposalsRes.ok) proposals = await proposalsRes.json();
|
||||
|
||||
const challengesRes = await apiFetch(`${API_URL}/challenges`);
|
||||
if (challengesRes.ok) challenges = await challengesRes.json();
|
||||
|
||||
loading = false;
|
||||
|
||||
const tickInterval = setInterval(() => { tick++; }, 1000);
|
||||
const pollInterval = setInterval(async () => {
|
||||
const [pRes, cRes] = await Promise.all([
|
||||
apiFetch(`${API_URL}/trade-proposals`),
|
||||
apiFetch(`${API_URL}/challenges`),
|
||||
]);
|
||||
if (pRes.ok) proposals = await pRes.json();
|
||||
if (cRes.ok) challenges = await cRes.json();
|
||||
}, 30_000);
|
||||
|
||||
return () => {
|
||||
clearInterval(tickInterval);
|
||||
clearInterval(pollInterval);
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
function formatExpiry(isoString: string) {
|
||||
const d = new Date(isoString);
|
||||
const diff = d.getTime() - Date.now();
|
||||
if (diff < 0) return 'expired';
|
||||
const hrs = Math.floor(diff / 3600000);
|
||||
if (hrs < 1) return 'in < 1h';
|
||||
if (hrs < 24) return `in ${hrs}h`;
|
||||
return `in ${Math.floor(hrs / 24)}d`;
|
||||
}
|
||||
|
||||
async function saveWishlist() {
|
||||
wishlistSaving = true;
|
||||
wishlistError = '';
|
||||
wishlistSaved = false;
|
||||
const res = await apiFetch(`${API_URL}/profile`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ trade_wishlist: wishlistText }),
|
||||
});
|
||||
wishlistSaving = false;
|
||||
if (res.ok) {
|
||||
wishlistSaved = true;
|
||||
setTimeout(() => { wishlistSaved = false; }, 2500);
|
||||
} else {
|
||||
wishlistError = 'Failed to save.';
|
||||
}
|
||||
}
|
||||
|
||||
function formatChallengeExpiry(isoString: string, _tick?: number) {
|
||||
const secs = Math.max(0, Math.floor((new Date(isoString).getTime() - Date.now()) / 1000));
|
||||
if (secs <= 0) return 'expired';
|
||||
if (secs < 60) return `${secs}s remaining`;
|
||||
return `${Math.floor(secs / 60)}m ${secs % 60}s remaining`;
|
||||
}
|
||||
|
||||
async function withdrawChallenge(challengeId: string) {
|
||||
const res = await apiFetch(`${API_URL}/challenges/${challengeId}/decline`, { method: 'POST' });
|
||||
if (res.ok) challenges = challenges.filter((c: any) => c.id !== challengeId);
|
||||
}
|
||||
|
||||
async function removeFriend(friendshipId: string) {
|
||||
await apiFetch(`${API_URL}/friendships/${friendshipId}`, { method: 'DELETE' });
|
||||
friends = friends.filter((f: any) => f.friendship_id !== friendshipId);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
@@ -69,7 +155,7 @@
|
||||
<span class="shards-icon">◈</span>
|
||||
<span class="shards-value">{profile.shards}</span>
|
||||
<span class="shards-label">Shards</span>
|
||||
<a href="/shards" class="shards-link">shatter cards</a>
|
||||
<a href="/shatter" class="shards-link">shatter cards</a>
|
||||
</div>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
@@ -98,6 +184,28 @@
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<div class="wishlist-group">
|
||||
<h2 class="section-title">Trade Wishlist</h2>
|
||||
<div class="wishlist-section">
|
||||
<p class="wishlist-hint">Cards or types you're looking to trade for. Visible on your public profile.</p>
|
||||
<textarea
|
||||
class="wishlist-textarea"
|
||||
bind:value={wishlistText}
|
||||
placeholder="e.g. Looking for legendary locations, rare scientists..."
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div class="wishlist-actions">
|
||||
<button class="save-btn" onclick={saveWishlist} disabled={wishlistSaving}>
|
||||
{wishlistSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
{#if wishlistSaved}<span class="wishlist-ok">Saved ✓</span>{/if}
|
||||
<p class="wishlist-error" style="min-height: 1.2em">{wishlistError}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<h2 class="section-title">Highlights</h2>
|
||||
<div class="highlights">
|
||||
<div class="highlight-card">
|
||||
@@ -129,17 +237,161 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<h2 class="section-title">Friends</h2>
|
||||
{#if friends.length === 0}
|
||||
<p class="no-friends">No friends yet.</p>
|
||||
{:else}
|
||||
<ul class="friends-list">
|
||||
{#each friends as f (f.friendship_id)}
|
||||
<li class="friend-item">
|
||||
<a href="/profile/{f.username}" class="friend-name">{f.username}</a>
|
||||
{#if confirmingRemove.has(f.friendship_id)}
|
||||
<span class="confirm-label">Remove friend?</span>
|
||||
<button class="confirm-yes-btn" onclick={() => removeFriend(f.friendship_id)}>Confirm</button>
|
||||
<button class="confirm-no-btn" onclick={() => { confirmingRemove.delete(f.friendship_id); confirmingRemove = confirmingRemove; }}>Cancel</button>
|
||||
{:else}
|
||||
<button class="unfriend-btn" onclick={() => { confirmingRemove.add(f.friendship_id); confirmingRemove = confirmingRemove; }}>Remove</button>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<h2 class="section-title">Trade Proposals</h2>
|
||||
|
||||
{#if proposals.length === 0}
|
||||
<p class="no-friends">No trade proposals.</p>
|
||||
{:else}
|
||||
{@const incoming = proposals.filter((p: any) => p.direction === 'incoming' && p.status === 'pending')}
|
||||
{@const outgoing = proposals.filter((p: any) => p.direction === 'outgoing' && p.status === 'pending')}
|
||||
{@const resolved = proposals.filter((p: any) => p.status !== 'pending')}
|
||||
|
||||
{#if incoming.length > 0}
|
||||
<p class="proposal-subhead">Incoming</p>
|
||||
{#each incoming as p (p.id)}
|
||||
<div class="proposal-card">
|
||||
<div class="proposal-meta">
|
||||
<a href="/profile/{p.proposer_username}" target="_blank" class="friend-name">{p.proposer_username}</a>
|
||||
<span class="proposal-status pending">Pending</span>
|
||||
<span class="proposal-expires">{formatExpiry(p.expires_at)}</span>
|
||||
</div>
|
||||
<p class="proposal-desc">
|
||||
{#if p.requested_cards.length > 0}Wants: <strong>{p.requested_cards.map((c: any) => c.name).join(', ')}</strong><br/>{/if}
|
||||
{#if p.offered_cards.length > 0}Offering: {p.offered_cards.map((c: any) => c.name).join(', ')}{/if}
|
||||
</p>
|
||||
<a href="/trade/proposal/{p.id}" class="see-proposal-btn">See Proposal</a>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if outgoing.length > 0}
|
||||
<p class="proposal-subhead">Outgoing</p>
|
||||
{#each outgoing as p (p.id)}
|
||||
<div class="proposal-card">
|
||||
<div class="proposal-meta">
|
||||
<span class="proposal-to">To: <a href="/profile/{p.recipient_username}" target="_blank" class="friend-name">{p.recipient_username}</a></span>
|
||||
<span class="proposal-status pending">Pending</span>
|
||||
<span class="proposal-expires">{formatExpiry(p.expires_at)}</span>
|
||||
</div>
|
||||
<p class="proposal-desc">
|
||||
{#if p.requested_cards.length > 0}Requesting: <strong>{p.requested_cards.map((c: any) => c.name).join(', ')}</strong><br/>{/if}
|
||||
{#if p.offered_cards.length > 0}Offering: {p.offered_cards.map((c: any) => c.name).join(', ')}{/if}
|
||||
</p>
|
||||
<a href="/trade/proposal/{p.id}" class="see-proposal-btn">See Proposal</a>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if resolved.length > 0}
|
||||
<p class="proposal-subhead">History</p>
|
||||
{#each resolved as p (p.id)}
|
||||
<div class="proposal-card resolved">
|
||||
<div class="proposal-meta">
|
||||
<span class="proposal-to">{p.direction === 'incoming' ? `From: ${p.proposer_username}` : `To: ${p.recipient_username}`}</span>
|
||||
<span class="proposal-status {p.status}">{p.status}</span>
|
||||
</div>
|
||||
<p class="proposal-desc">{p.requested_cards.length} requested · {p.offered_cards.length} offered</p>
|
||||
<a href="/trade/proposal/{p.id}" class="see-proposal-btn">See Proposal</a>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
|
||||
{/if}
|
||||
|
||||
{#if challenges.length > 0}
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<h2 class="section-title">Game Challenges</h2>
|
||||
|
||||
{@const pendingOut = challenges.filter((c: any) => c.direction === 'outgoing' && c.status === 'pending')}
|
||||
{@const pendingIn = challenges.filter((c: any) => c.direction === 'incoming' && c.status === 'pending')}
|
||||
{@const resolvedC = challenges.filter((c: any) => c.status !== 'pending')}
|
||||
|
||||
{#if pendingOut.length > 0}
|
||||
<p class="proposal-subhead">Sent</p>
|
||||
{#each pendingOut as c (c.id)}
|
||||
<div class="proposal-card">
|
||||
<div class="proposal-meta">
|
||||
<span class="proposal-to">To: <a href="/profile/{c.challenged_username}" target="_blank" class="friend-name">{c.challenged_username}</a></span>
|
||||
<span class="proposal-status pending">Awaiting response</span>
|
||||
<span class="proposal-expires">{formatChallengeExpiry(c.expires_at, tick)}</span>
|
||||
</div>
|
||||
<p class="proposal-desc">Deck: <strong>{c.deck_name}</strong></p>
|
||||
{#if confirmingWithdraw.has(c.id)}
|
||||
<span class="confirm-label">Withdraw challenge?</span>
|
||||
<button class="confirm-yes-btn" onclick={() => withdrawChallenge(c.id)}>Confirm</button>
|
||||
<button class="confirm-no-btn" onclick={() => { confirmingWithdraw.delete(c.id); confirmingWithdraw = confirmingWithdraw; }}>Cancel</button>
|
||||
{:else}
|
||||
<button class="withdraw-btn" onclick={() => { confirmingWithdraw.add(c.id); confirmingWithdraw = confirmingWithdraw; }}>Withdraw</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if pendingIn.length > 0}
|
||||
<p class="proposal-subhead">Incoming</p>
|
||||
{#each pendingIn as c (c.id)}
|
||||
<div class="proposal-card">
|
||||
<div class="proposal-meta">
|
||||
<a href="/profile/{c.challenger_username}" target="_blank" class="friend-name">{c.challenger_username}</a>
|
||||
<span class="proposal-status pending">Pending</span>
|
||||
<span class="proposal-expires">{formatChallengeExpiry(c.expires_at, tick)}</span>
|
||||
</div>
|
||||
<p class="proposal-desc">Their deck: <strong>{c.deck_name}</strong></p>
|
||||
<p class="proposal-desc">Check your notification bell to accept.</p>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if resolvedC.length > 0}
|
||||
<p class="proposal-subhead">History</p>
|
||||
{#each resolvedC as c (c.id)}
|
||||
<div class="proposal-card resolved">
|
||||
<div class="proposal-meta">
|
||||
<span class="proposal-to">
|
||||
{c.direction === 'outgoing' ? `To: ${c.challenged_username}` : `From: ${c.challenger_username}`}
|
||||
</span>
|
||||
<span class="proposal-status {c.status}">{c.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
</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;
|
||||
background: var(--color-bg);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
@@ -149,6 +401,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
@@ -160,16 +413,16 @@
|
||||
.avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: #3d2507;
|
||||
border: 2px solid #c8861a;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-surface-raised);
|
||||
border: 2px solid var(--color-bronze);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 28px;
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
color: #f0d080;
|
||||
color: var(--color-gold);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -182,22 +435,22 @@
|
||||
|
||||
.username {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 24px;
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 700;
|
||||
color: #f0d080;
|
||||
color: var(--color-gold);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.email {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 15px;
|
||||
font-size: var(--text-md);
|
||||
color: rgba(240, 180, 80, 0.5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.unverified-badge {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 9px;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
@@ -211,7 +464,7 @@
|
||||
|
||||
.resend-btn {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 13px;
|
||||
font-size: var(--text-base);
|
||||
font-style: italic;
|
||||
color: rgba(240, 180, 80, 0.5);
|
||||
background: none;
|
||||
@@ -228,7 +481,7 @@
|
||||
|
||||
.joined {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 13px;
|
||||
font-size: var(--text-base);
|
||||
font-style: italic;
|
||||
color: rgba(240, 180, 80, 0.35);
|
||||
margin: 0;
|
||||
@@ -236,29 +489,29 @@
|
||||
|
||||
.logout-btn {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 11px;
|
||||
font-size: var(--btn-font-md);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
background: none;
|
||||
border: 1px solid rgba(180, 60, 60, 0.4);
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: rgba(200, 80, 80, 0.7);
|
||||
padding: 8px 16px;
|
||||
padding: var(--btn-padding-md);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
border-color: #c84040;
|
||||
border-color: var(--color-error);
|
||||
color: #e05050;
|
||||
background: rgba(180, 40, 40, 0.1);
|
||||
}
|
||||
|
||||
.reset-link {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 14px;
|
||||
font-size: var(--text-base);
|
||||
font-style: italic;
|
||||
color: rgba(240, 180, 80, 0.4);
|
||||
text-decoration: underline;
|
||||
@@ -276,38 +529,39 @@
|
||||
|
||||
.shards-link {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 10px;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(126, 207, 207, 0.6);
|
||||
border: 1px solid rgba(126, 207, 207, 0.3);
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 3px 8px;
|
||||
text-decoration: none;
|
||||
margin-top: 4px;
|
||||
margin-left: 0.5rem;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.shards-link:hover { color: #7ecfcf; border-color: rgba(126, 207, 207, 0.7); }
|
||||
.shards-link:hover { color: var(--color-cyan); border-color: rgba(126, 207, 207, 0.7); }
|
||||
|
||||
.shards-icon {
|
||||
font-size: 22px;
|
||||
color: #7ecfcf;
|
||||
font-size: var(--text-xl);
|
||||
color: var(--color-cyan);
|
||||
position: relative;
|
||||
top: -0.1em;
|
||||
animation: shard-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shards-value {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 28px;
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
color: #7ecfcf;
|
||||
color: var(--color-cyan);
|
||||
}
|
||||
|
||||
.shards-label {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 11px;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
@@ -317,16 +571,16 @@
|
||||
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: rgba(107, 76, 30, 0.3);
|
||||
background: var(--color-border-dim);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 13px;
|
||||
font-size: var(--text-base);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(240, 180, 80, 0.4);
|
||||
color: var(--color-gold-faint);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -337,9 +591,9 @@
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #1a1008;
|
||||
border: 1px solid rgba(107, 76, 30, 0.3);
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-dim);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -348,24 +602,24 @@
|
||||
|
||||
.stat-label {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 9px;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(240, 180, 80, 0.4);
|
||||
color: var(--color-gold-faint);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 28px;
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
color: #f0d080;
|
||||
color: var(--color-gold);
|
||||
}
|
||||
|
||||
.wins { color: #6aaa6a; }
|
||||
.losses { color: #c85050; }
|
||||
.good-wr { color: #6aaa6a; }
|
||||
.bad-wr { color: #c85050; }
|
||||
.wins { color: var(--color-success); }
|
||||
.losses { color: var(--color-error); }
|
||||
.good-wr { color: var(--color-success); }
|
||||
.bad-wr { color: var(--color-error); }
|
||||
|
||||
.highlights {
|
||||
display: grid;
|
||||
@@ -374,9 +628,9 @@
|
||||
}
|
||||
|
||||
.highlight-card {
|
||||
background: #1a1008;
|
||||
border: 1px solid rgba(107, 76, 30, 0.3);
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-dim);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -385,23 +639,23 @@
|
||||
|
||||
.highlight-label {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 9px;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(240, 180, 80, 0.4);
|
||||
color: var(--color-gold-faint);
|
||||
}
|
||||
|
||||
.highlight-value {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 16px;
|
||||
font-size: var(--text-md);
|
||||
font-weight: 700;
|
||||
color: #f0d080;
|
||||
color: var(--color-gold);
|
||||
}
|
||||
|
||||
.highlight-sub {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 13px;
|
||||
font-size: var(--text-base);
|
||||
font-style: italic;
|
||||
color: rgba(240, 180, 80, 0.45);
|
||||
}
|
||||
@@ -418,8 +672,8 @@
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(107, 76, 30, 0.4);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -429,22 +683,303 @@
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.wishlist-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.wishlist-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wishlist-hint {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: var(--text-base);
|
||||
font-style: italic;
|
||||
color: rgba(240, 180, 80, 0.35);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wishlist-textarea {
|
||||
width: 100%;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-gold);
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: var(--text-md);
|
||||
padding: 0.6rem 0.75rem;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.15s;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.wishlist-textarea:focus { border-color: var(--color-bronze); }
|
||||
.wishlist-textarea::placeholder { color: rgba(240, 180, 80, 0.25); }
|
||||
|
||||
.wishlist-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: var(--btn-font-md);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
background: var(--color-surface-raised);
|
||||
border: 1px solid var(--color-bronze);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-btn-text);
|
||||
padding: var(--btn-padding-md);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.save-btn:hover:not(:disabled) { background: #4d3010; }
|
||||
.save-btn:disabled { opacity: 0.5; cursor: default; }
|
||||
|
||||
.wishlist-ok {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.wishlist-error {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-error);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.no-friends {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: var(--text-base);
|
||||
font-style: italic;
|
||||
color: rgba(240, 180, 80, 0.25);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.friends-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.friend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-dim);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.friend-name {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: var(--text-base);
|
||||
font-weight: 700;
|
||||
color: var(--color-gold);
|
||||
text-decoration: none;
|
||||
flex: 1;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.friend-name:hover { color: var(--color-bronze); }
|
||||
|
||||
.unfriend-btn {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: var(--btn-font-md);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
background: rgba(180, 40, 40, 0.8);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: #fff;
|
||||
padding: var(--btn-padding-md);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.unfriend-btn:hover { background: rgba(210, 50, 50, 0.9); }
|
||||
|
||||
.no-data {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 14px;
|
||||
font-size: var(--text-base);
|
||||
font-style: italic;
|
||||
color: rgba(240, 180, 80, 0.25);
|
||||
}
|
||||
|
||||
/* ── Trade Proposals ── */
|
||||
.proposal-subhead {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(240, 180, 80, 0.3);
|
||||
margin: 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.proposal-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-dim);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 0.85rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.proposal-card.resolved { opacity: 0.5; }
|
||||
|
||||
.proposal-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.proposal-to {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: var(--text-base);
|
||||
color: rgba(240, 180, 80, 0.5);
|
||||
}
|
||||
|
||||
.proposal-status {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.proposal-status.pending { color: var(--color-bronze); border-color: rgba(200, 134, 26, 0.4); }
|
||||
.proposal-status.accepted { color: var(--color-success); border-color: rgba(106, 170, 106, 0.4); }
|
||||
.proposal-status.declined { color: var(--color-error); border-color: rgba(200, 64, 64, 0.4); }
|
||||
.proposal-status.expired { color: rgba(240, 180, 80, 0.3); border-color: var(--color-border-dim); }
|
||||
.proposal-status.withdrawn { color: rgba(240, 180, 80, 0.3); border-color: var(--color-border-dim); }
|
||||
|
||||
.proposal-expires {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: var(--text-sm);
|
||||
font-style: italic;
|
||||
color: rgba(240, 180, 80, 0.3);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.proposal-desc {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: var(--text-base);
|
||||
color: rgba(240, 180, 80, 0.65);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.proposal-desc strong { color: var(--color-gold); font-style: normal; font-weight: 600; }
|
||||
|
||||
.see-proposal-btn {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: var(--btn-font-md);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
background: var(--color-surface-raised);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-gold);
|
||||
padding: var(--btn-padding-md);
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: all 0.15s;
|
||||
margin-top: 0.25rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.see-proposal-btn:hover { border-color: var(--color-bronze); background: #4d3010; }
|
||||
|
||||
.withdraw-btn {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: var(--btn-font-sm);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
background: none;
|
||||
border: 1px solid rgba(180, 60, 60, 0.4);
|
||||
border-radius: var(--radius-sm);
|
||||
color: rgba(200, 80, 80, 0.6);
|
||||
padding: var(--btn-padding-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.withdraw-btn:hover { border-color: var(--color-error); color: #e05050; }
|
||||
|
||||
.status {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 16px;
|
||||
font-size: var(--text-md);
|
||||
font-style: italic;
|
||||
color: rgba(240, 180, 80, 0.5);
|
||||
text-align: center;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.confirm-label {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: var(--text-base);
|
||||
font-style: italic;
|
||||
color: rgba(200, 80, 80, 0.8);
|
||||
}
|
||||
|
||||
.confirm-yes-btn {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: var(--btn-font-sm);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
background: rgba(180, 40, 40, 0.8);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #fff;
|
||||
padding: var(--btn-padding-sm);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.confirm-yes-btn:hover { background: rgba(210, 50, 50, 0.9); }
|
||||
|
||||
.confirm-no-btn {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: var(--btn-font-sm);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
background: none;
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-gold-faint);
|
||||
padding: var(--btn-padding-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.confirm-no-btn:hover { border-color: rgba(107, 76, 30, 0.7); color: rgba(240, 180, 80, 0.7); }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.highlights { grid-template-columns: 1fr; }
|
||||
|
||||
Reference in New Issue
Block a user