From 8e86b65a45ca7df12d24c1d2a34ea817ef5c8a7b Mon Sep 17 00:00:00 2001 From: pleb Date: Wed, 29 Oct 2025 22:22:15 -0700 Subject: [PATCH] add player card to compare player form --- AGENTS.md | 7 +- src/lib/components/HasToolAccess.svelte | 17 +- src/lib/components/NavBar.svelte | 14 +- src/lib/components/PlayerCard.svelte | 149 ++++++++++----- src/lib/components/PlayerCompareForm.svelte | 172 +++++++++++++++--- src/lib/utils/plebsaber-utils.ts | 2 +- .../{testing => player-info}/+page.svelte | 157 ++++++++-------- src/routes/tools/+page.svelte | 2 +- .../+page.svelte | 102 ++++++++++- src/routes/tools/stats/+page.server.ts | 57 +++++- src/routes/tools/stats/+page.svelte | 116 ++++++------ 11 files changed, 559 insertions(+), 236 deletions(-) rename src/routes/{testing => player-info}/+page.svelte (52%) rename src/routes/tools/{beatleader-compare => compare-histories}/+page.svelte (76%) diff --git a/AGENTS.md b/AGENTS.md index 1dbe858..55d81d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,9 +8,10 @@ - OAuth callback stores identity in a long-lived session (`plebsaber_session` via `.data/plebsaber_sessions.json`). - Tools expect BeatLeader supporter or global rank better than pleb's current rank; navbar dropdown warns when outside that threshold. - Global navigation lives in `src/lib/components/NavBar.svelte`; it houses the BeatLeader login dropdown with profile, testing (dev) and logout actions. -- Core BeatLeader tool pages reuse shared components such as `PlayerCompareForm.svelte`, `MapCard.svelte`, and `SongPlayer.svelte` for consistent UI/UX. -- `/testing` route (dev-only) consumes `/api/beatleader/me` to display and log raw BeatLeader session data for debugging. -- `/tools/stats` (admin-only, BL id `76561199407393962`) lists every stored BeatLeader session with avatars and last-seen timestamps; navigation link only appears for that account. +- Core BeatLeader tool pages reuse shared components such as `PlayerCompareForm.svelte`, `MapCard.svelte`, `PlayerCard.svelte`, and `SongPlayer.svelte` for consistent UI/UX. +- `/player-info` route consumes `/api/beatleader/me` to display and log raw BeatLeader session data for debugging. +- `/tools/stats` (admin-only, BL id `76561199407393962`) lists every stored BeatLeader session with full player profiles, skill triangles, and last-seen timestamps; navigation link only appears for that account. +- `/tools/compare-histories` compares two players' play histories and displays their profiles with skill triangles when comparing. ## Shell command guidance diff --git a/src/lib/components/HasToolAccess.svelte b/src/lib/components/HasToolAccess.svelte index aee5f82..68114f6 100644 --- a/src/lib/components/HasToolAccess.svelte +++ b/src/lib/components/HasToolAccess.svelte @@ -1,6 +1,5 @@ @@ -43,14 +35,8 @@ {:else}

- Tools are restricted to BeatLeader supporters (and {baselineCopy}). + Auth Required: Tools are restricted to BeatLeader supporters (and {baselineCopy}).

- {#if summary} -

{summary}

- {/if} - {#if showLockedMessage} -

{showLockedMessage}

- {/if} {#if plebProfile}
diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte index 77d4d5e..b0dbd9e 100644 --- a/src/lib/components/NavBar.svelte +++ b/src/lib/components/NavBar.svelte @@ -1,6 +1,5 @@ -
- Avatar -
-
{name}
-
- {#if country} - {country} - {/if} - {#if showRank && typeof rank === 'number'} - Rank: {rank} - {/if} +{#if profileUrl} + + Avatar +
+
{name}
+
+ {#if country} + {country} + {/if} + {#if showRank && typeof rank === 'number'} + Rank: {rank} + {/if} +
+ {#if hasTriangle} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {/if} +
+{:else} +
+ Avatar +
+
{name}
+
+ {#if country} + {country} + {/if} + {#if showRank && typeof rank === 'number'} + Rank: {rank} + {/if} +
+
+ {#if hasTriangle} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {/if}
- {#if hasTriangle} -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- {/if} -
+{/if} diff --git a/src/lib/components/PlayerCompareForm.svelte b/src/lib/components/PlayerCompareForm.svelte index 09c7a94..0f24df7 100644 --- a/src/lib/components/PlayerCompareForm.svelte +++ b/src/lib/components/PlayerCompareForm.svelte @@ -1,6 +1,8 @@ -
-
- + +
+
+
+ +
+ {#if $$slots['player-a-card']} + + {:else if hasCompared} +
+ {#if playerAProfile} + + {:else} +
Player A profile not found
+ {/if} +
+ {/if} +
+ +
+
+ +
+ {#if $$slots['player-b-card']} + + {:else if hasCompared} +
+ {#if playerBProfile} + + {:else} +
Player B profile not found
+ {/if} +
+ {/if} +
-
- -
-
+ +
@@ -327,6 +400,27 @@ background: #0f172a; color: #fff; } + + .player-card-wrapper { + border: 1px solid rgba(148, 163, 184, 0.18); + border-radius: 0.75rem; + padding: 1.25rem; + background: linear-gradient(160deg, rgba(15, 23, 42, 0.6), rgba(8, 12, 24, 0.85)); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(34, 211, 238, 0.05); + transition: border-color 0.2s ease, box-shadow 0.2s ease; + } + + .player-card-wrapper:hover { + border-color: rgba(34, 211, 238, 0.25); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35), inset 0 0 0 1px rgba(34, 211, 238, 0.15); + } + + .empty-profile { + padding: 2rem; + text-align: center; + color: rgba(148, 163, 184, 0.7); + font-size: 0.9rem; + } diff --git a/src/routes/tools/stats/+page.server.ts b/src/routes/tools/stats/+page.server.ts index ef675c9..3ee8cbb 100644 --- a/src/routes/tools/stats/+page.server.ts +++ b/src/routes/tools/stats/+page.server.ts @@ -7,10 +7,36 @@ type StatsUser = { beatleaderId: string; name: string | null; avatar: string | null; + country: string | null; + rank: number | null; + techPp: number | null; + accPp: number | null; + passPp: number | null; lastSeenAt: number; lastSeenIso: string; }; +type BeatLeaderPlayer = { + id: string; + name: string; + avatar?: string; + country?: string; + rank?: number; + techPp?: number; + accPp?: number; + passPp?: number; +}; + +async function fetchBeatLeaderProfile(playerId: string): Promise { + try { + const response = await fetch(`https://api.beatleader.xyz/player/${playerId}`); + if (!response.ok) return null; + return (await response.json()) as BeatLeaderPlayer; + } catch { + return null; + } +} + export const load: PageServerLoad = async ({ cookies }) => { const currentSession = getSession(cookies); if (!currentSession) { @@ -22,22 +48,33 @@ export const load: PageServerLoad = async ({ cookies }) => { } const sessions = getAllSessions(); - const aggregated = new Map(); + const aggregated = new Map(); for (const stored of sessions) { const existing = aggregated.get(stored.beatleaderId); - if (!existing || stored.lastSeenAt > existing.lastSeenAt) { - aggregated.set(stored.beatleaderId, { - beatleaderId: stored.beatleaderId, - name: stored.name, - avatar: stored.avatar, - lastSeenAt: stored.lastSeenAt, - lastSeenIso: new Date(stored.lastSeenAt).toISOString() - }); + if (!existing || stored.lastSeenAt > existing.session.lastSeenAt) { + aggregated.set(stored.beatleaderId, { session: stored }); } } - const users = Array.from(aggregated.values()).sort((a, b) => b.lastSeenAt - a.lastSeenAt); + // Fetch all player profiles in parallel + const userPromises = Array.from(aggregated.values()).map(async ({ session }) => { + const profile = await fetchBeatLeaderProfile(session.beatleaderId); + return { + beatleaderId: session.beatleaderId, + name: profile?.name ?? session.name, + avatar: profile?.avatar ?? session.avatar, + country: profile?.country ?? null, + rank: profile?.rank ?? null, + techPp: profile?.techPp ?? null, + accPp: profile?.accPp ?? null, + passPp: profile?.passPp ?? null, + lastSeenAt: session.lastSeenAt, + lastSeenIso: new Date(session.lastSeenAt).toISOString() + } satisfies StatsUser; + }); + + const users = (await Promise.all(userPromises)).sort((a, b) => b.lastSeenAt - a.lastSeenAt); return { users diff --git a/src/routes/tools/stats/+page.svelte b/src/routes/tools/stats/+page.svelte index 5ccb03d..8982be1 100644 --- a/src/routes/tools/stats/+page.svelte +++ b/src/routes/tools/stats/+page.svelte @@ -1,6 +1,6 @@ - PLEBSABER · Stats + plebsaber · Stats
@@ -60,20 +60,35 @@
{:else}
    - {#each data.users as user} + {#each data.users as user, index}
  • - {user.name -
    -
    {user.name ?? 'Unknown BeatLeader user'}
    -
    ID: {user.beatleaderId}
    - +
    + +
    +
    +
    + ID + {user.beatleaderId} +
    +
    + Last Session + +
  • {/each} @@ -121,8 +136,8 @@ .user-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 1.25rem; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 1.5rem; padding: 0; margin: 0; list-style: none; @@ -130,65 +145,62 @@ .user-card { display: flex; + flex-direction: column; gap: 1rem; - padding: 1.1rem; + padding: 1.25rem; border-radius: 0.85rem; border: 1px solid rgba(148, 163, 184, 0.18); background: linear-gradient(160deg, rgba(15, 23, 42, 0.6), rgba(8, 12, 24, 0.85)); - box-shadow: inset 0 0 0 1px rgba(34, 211, 238, 0.05); - transition: transform 0.2s ease, border-color 0.2s ease; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(34, 211, 238, 0.05); + transition: border-color 0.2s ease, box-shadow 0.2s ease; } .user-card:hover, .user-card:focus-within { - transform: translateY(-2px); border-color: rgba(34, 211, 238, 0.35); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35), inset 0 0 0 1px rgba(34, 211, 238, 0.15); } - .user-avatar { - width: 64px; - height: 64px; - border-radius: 50%; - object-fit: cover; - border: 2px solid rgba(34, 211, 238, 0.35); - box-shadow: 0 6px 18px rgba(15, 23, 42, 0.45); + .card-header { + display: flex; + width: 100%; } - .user-details { + .user-meta { display: grid; - gap: 0.35rem; - align-content: center; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid rgba(148, 163, 184, 0.15); } - .user-name { + .meta-item { + display: flex; + flex-direction: column; + gap: 0.25rem; + } + + .meta-label { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(148, 163, 184, 0.6); font-weight: 600; - color: rgba(248, 250, 252, 0.95); } - .user-id { - font-size: 0.8rem; - color: rgba(148, 163, 184, 0.7); - } - - .user-seen { + .meta-value { font-size: 0.85rem; - color: rgba(34, 211, 238, 0.75); + color: rgba(226, 232, 240, 0.9); + word-break: break-all; } @media (max-width: 640px) { - .user-card { - flex-direction: column; - align-items: center; - text-align: center; + .user-grid { + grid-template-columns: 1fr; } - .user-details { - align-items: center; - } - - .user-avatar { - width: 56px; - height: 56px; + .user-meta { + grid-template-columns: 1fr; } }