fix: Fix player profile loading with API transformer and improve UI layout
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<PlayerMeta> {
|
||||
const url = `/player/${steamId}/meta/${limit}`;
|
||||
const data = await apiClient.get<PlayerMeta>(url);
|
||||
// Use the /player/{id} endpoint which has the data we need
|
||||
const url = `/player/${steamId}`;
|
||||
const legacyData = await apiClient.get<LegacyPlayerProfile>(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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
multi_kills?: Record<string, number>;
|
||||
dmg?: Record<string, unknown>;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -247,7 +247,7 @@
|
||||
</div>
|
||||
|
||||
{#if recentMatches.length > 0}
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
{#each recentMatches as match}
|
||||
<MatchCard {match} />
|
||||
{/each}
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user