From a861b1c1b6faf3fcf708c0c120a38f53ef9bb870 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Wed, 5 Nov 2025 00:43:50 +0100 Subject: [PATCH] fix: Fix player profile loading with API transformer and improve UI layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add LegacyPlayerProfile transformer to handle API response format mismatch - Transform avatar hashes to full Steam CDN URLs - Map team IDs correctly (API 1/2 -> Schema 2/3) - Calculate aggregate stats (avg_kills, avg_deaths, win_rate) from matches - Reduce featured matches on homepage from 6 to 3 - Show 4 recent matches on player profile instead of 10 - Display recent matches in 4-column grid on desktop (side-by-side) Fixes "Player not found" error for all player profiles. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/lib/api/players.ts | 54 ++++++++++++++-- src/lib/api/transformers.ts | 98 +++++++++++++++++++++++++++++ src/routes/+page.ts | 6 +- src/routes/player/[id]/+page.svelte | 2 +- src/routes/player/[id]/+page.ts | 2 +- 5 files changed, 152 insertions(+), 10 deletions(-) diff --git a/src/lib/api/players.ts b/src/lib/api/players.ts index 8979f77..6c4ddab 100644 --- a/src/lib/api/players.ts +++ b/src/lib/api/players.ts @@ -1,6 +1,7 @@ import { apiClient } from './client'; -import { parsePlayer, parsePlayerMeta } from '$lib/schemas'; +import { parsePlayer } from '$lib/schemas'; import type { Player, PlayerMeta, TrackPlayerResponse } from '$lib/types'; +import { transformPlayerProfile, type LegacyPlayerProfile } from './transformers'; /** * Player API endpoints @@ -27,11 +28,54 @@ export const playersAPI = { * @returns Player metadata */ async getPlayerMeta(steamId: string, limit = 10): Promise { - const url = `/player/${steamId}/meta/${limit}`; - const data = await apiClient.get(url); + // Use the /player/{id} endpoint which has the data we need + const url = `/player/${steamId}`; + const legacyData = await apiClient.get(url); - // Validate with Zod schema - return parsePlayerMeta(data); + // Transform legacy API format to our schema format + const transformedData = transformPlayerProfile(legacyData); + + // Validate the player data + const player = parsePlayer(transformedData); + + // Calculate aggregated stats from matches + const matches = player.matches || []; + const recentMatches = matches.slice(0, limit); + + const totalKills = recentMatches.reduce((sum, m) => sum + (m.stats?.kills || 0), 0); + const totalDeaths = recentMatches.reduce((sum, m) => sum + (m.stats?.deaths || 0), 0); + const totalKast = recentMatches.reduce((sum, _m) => { + // KAST is a percentage, we need to calculate it + // For now, we'll use a placeholder + return sum + 0; + }, 0); + + const wins = recentMatches.filter((m) => { + // match_result 1 = win, 2 = loss + return m.match_result === 1; + }).length; + + const avgKills = recentMatches.length > 0 ? totalKills / recentMatches.length : 0; + const avgDeaths = recentMatches.length > 0 ? totalDeaths / recentMatches.length : 0; + const winRate = recentMatches.length > 0 ? wins / recentMatches.length : 0; + + // Find the most recent match date + const lastMatchDate = matches.length > 0 ? matches[0].date : new Date().toISOString(); + + // Transform to PlayerMeta format + const playerMeta: PlayerMeta = { + id: parseInt(player.id), + name: player.name, + avatar: player.avatar, // Already transformed by transformPlayerProfile + recent_matches: recentMatches.length, + last_match_date: lastMatchDate, + avg_kills: avgKills, + avg_deaths: avgDeaths, + avg_kast: totalKast / recentMatches.length || 0, // Placeholder KAST calculation + win_rate: winRate + }; + + return playerMeta; }, /** diff --git a/src/lib/api/transformers.ts b/src/lib/api/transformers.ts index f2fe933..c9b91cf 100644 --- a/src/lib/api/transformers.ts +++ b/src/lib/api/transformers.ts @@ -161,3 +161,101 @@ export function transformMatchDetail(legacy: LegacyMatchDetail): Match { players: legacy.stats?.map(transformPlayerStats) }; } + +/** + * Legacy player profile format from API + */ +export interface LegacyPlayerProfile { + steamid64: string; + name: string; + avatar: string; // Hash, not full URL + vac: boolean; + vac_date: number; // Unix timestamp + game_ban: boolean; + game_ban_date: number; // Unix timestamp + tracked: boolean; + match_stats?: { + win: number; + loss: number; + }; + matches?: Array<{ + match_id: string; + map: string; + date: number; + score: [number, number]; + duration: number; + match_result: number; + max_rounds: number; + parsed: boolean; + vac: boolean; + game_ban: boolean; + stats: { + team_id: number; + kills: number; + deaths: number; + assists: number; + headshot: number; + mvp: number; + score: number; + rank: Record; + multi_kills?: Record; + dmg?: Record; + }; + }>; +} + +/** + * Transform legacy player profile to schema-compatible format + */ +export function transformPlayerProfile(legacy: LegacyPlayerProfile) { + // Unix timestamp -62135596800 represents "no date" (year 0) + const hasVacDate = legacy.vac_date && legacy.vac_date > 0; + const hasGameBanDate = legacy.game_ban_date && legacy.game_ban_date > 0; + + return { + id: legacy.steamid64, + name: legacy.name, + avatar: `https://avatars.steamstatic.com/${legacy.avatar}_full.jpg`, + steam_updated: new Date().toISOString(), // Not provided by API + vac_count: legacy.vac ? 1 : 0, + vac_date: hasVacDate ? new Date(legacy.vac_date * 1000).toISOString() : null, + game_ban_count: legacy.game_ban ? 1 : 0, + game_ban_date: hasGameBanDate ? new Date(legacy.game_ban_date * 1000).toISOString() : null, + wins: legacy.match_stats?.win, + losses: legacy.match_stats?.loss, + matches: legacy.matches?.map((match) => ({ + match_id: match.match_id, + map: match.map || 'unknown', + date: new Date(match.date * 1000).toISOString(), + score_team_a: match.score[0], + score_team_b: match.score[1], + duration: match.duration, + match_result: match.match_result, + max_rounds: match.max_rounds, + demo_parsed: match.parsed, + vac_present: match.vac, + gameban_present: match.game_ban, + tick_rate: 64, // Not provided by API + stats: { + id: legacy.steamid64, + name: legacy.name, + avatar: `https://avatars.steamstatic.com/${legacy.avatar}_full.jpg`, + // Fix team_id: API returns 1/2, but schema expects min 2 + // Map: 1 -> 2 (Terrorists), 2 -> 3 (Counter-Terrorists) + team_id: + match.stats.team_id === 1 ? 2 : match.stats.team_id === 2 ? 3 : match.stats.team_id, + kills: match.stats.kills, + deaths: match.stats.deaths, + assists: match.stats.assists, + headshot: match.stats.headshot, + mvp: match.stats.mvp, + score: match.stats.score, + kast: 0, + mk_2: match.stats.multi_kills?.duo, + mk_3: match.stats.multi_kills?.triple, + mk_4: match.stats.multi_kills?.quad, + mk_5: match.stats.multi_kills?.ace + } + })) + }; +} diff --git a/src/routes/+page.ts b/src/routes/+page.ts index 0d86f98..e5069ac 100644 --- a/src/routes/+page.ts +++ b/src/routes/+page.ts @@ -10,11 +10,11 @@ export const load: PageLoad = async ({ parent }) => { await parent(); try { - // Load featured matches (limit to 6 for homepage) - const matchesData = await api.matches.getMatches({ limit: 6 }); + // Load featured matches (limit to 3 for homepage) + const matchesData = await api.matches.getMatches({ limit: 3 }); return { - featuredMatches: matchesData.matches.slice(0, 6), // Ensure max 6 matches + featuredMatches: matchesData.matches.slice(0, 3), // Ensure max 3 matches meta: { title: 'CS2.WTF - Statistics for CS2 Matchmaking', description: diff --git a/src/routes/player/[id]/+page.svelte b/src/routes/player/[id]/+page.svelte index 6fd8564..ab3ed20 100644 --- a/src/routes/player/[id]/+page.svelte +++ b/src/routes/player/[id]/+page.svelte @@ -247,7 +247,7 @@ {#if recentMatches.length > 0} -
+
{#each recentMatches as match} {/each} diff --git a/src/routes/player/[id]/+page.ts b/src/routes/player/[id]/+page.ts index f3fccf5..3cc73be 100644 --- a/src/routes/player/[id]/+page.ts +++ b/src/routes/player/[id]/+page.ts @@ -45,7 +45,7 @@ export const load: PageLoad = async ({ params }) => { return { profile, - recentMatches: matchesData.matches.slice(0, 10), // Show 10 in recent matches section + recentMatches: matchesData.matches.slice(0, 4), // Show 4 in recent matches section playerStats, // Full stats for charts meta: { title: `${profile.name} - Player Profile | CS2.WTF`,