fix: Add API response transformer for legacy CSGOW.TF format

- Create transformers.ts to convert legacy API format to new schema
- Transform score array [a, b] to score_team_a/score_team_b fields
- Convert Unix timestamps to ISO strings
- Map legacy field names (parsed, vac, game_ban) to new names
- Update matches API to use transformer with proper types
- Handle empty map names gracefully in homepage
- Limit featured matches to exactly 6 items

Fixes homepage data display issue where API format mismatch prevented
matches from rendering. API returns legacy CSGO:WTF format while frontend
expects new CS2.WTF schema.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-04 23:14:28 +01:00
parent 8b73a68a6b
commit ea61061530
4 changed files with 68 additions and 13 deletions

View File

@@ -1,12 +1,12 @@
import { apiClient } from './client';
import {
parseMatch,
parseMatchesList,
parseMatchRounds,
parseMatchWeapons,
parseMatchChat,
parseMatchParseResponse
} from '$lib/schemas';
import { transformMatchesListResponse, type LegacyMatchListItem } from './transformers';
import type {
Match,
MatchesListResponse,
@@ -94,7 +94,8 @@ export const matchesAPI = {
async getMatches(params?: MatchesQueryParams): Promise<MatchesListResponse> {
const url = params?.before_time ? `/matches/next/${params.before_time}` : '/matches';
const data = await apiClient.get<MatchesListResponse>(url, {
// API returns a plain array, not a wrapped object
const data = await apiClient.get<LegacyMatchListItem[]>(url, {
params: {
limit: params?.limit,
map: params?.map,
@@ -102,8 +103,8 @@ export const matchesAPI = {
}
});
// Validate with Zod schema
return parseMatchesList(data);
// Transform legacy API response to new format
return transformMatchesListResponse(data);
},
/**
@@ -113,7 +114,8 @@ export const matchesAPI = {
*/
async searchMatches(params?: MatchesQueryParams): Promise<MatchesListResponse> {
const url = '/matches';
const data = await apiClient.getCancelable<MatchesListResponse>(url, 'match-search', {
// API returns a plain array, not a wrapped object
const data = await apiClient.getCancelable<LegacyMatchListItem[]>(url, 'match-search', {
params: {
limit: params?.limit || 20,
map: params?.map,
@@ -122,8 +124,8 @@ export const matchesAPI = {
}
});
// Validate with Zod schema
return parseMatchesList(data);
// Transform legacy API response to new format
return transformMatchesListResponse(data);
},
/**

View File

@@ -0,0 +1,51 @@
/**
* API Response Transformers
* Converts legacy CSGO:WTF API responses to the new CS2.WTF format
*/
import type { MatchListItem, MatchesListResponse } from '$lib/types';
/**
* Legacy API match format (from api.csgow.tf)
*/
export interface LegacyMatchListItem {
match_id: string;
map: string;
date: number; // Unix timestamp
score: [number, number]; // [team_a, team_b]
duration: number;
match_result: number;
max_rounds: number;
parsed: boolean;
vac: boolean;
game_ban: boolean;
}
/**
* Transform legacy match list item to new format
*/
export function transformMatchListItem(legacy: LegacyMatchListItem): MatchListItem {
return {
match_id: Number(legacy.match_id),
map: legacy.map || 'unknown', // Handle empty map names
date: new Date(legacy.date * 1000).toISOString(), // Convert Unix timestamp to ISO string
score_team_a: legacy.score[0],
score_team_b: legacy.score[1],
duration: legacy.duration,
demo_parsed: legacy.parsed,
player_count: 10 // Default to 10 players (5v5)
};
}
/**
* Transform legacy matches list response to new format
*/
export function transformMatchesListResponse(
legacyMatches: LegacyMatchListItem[]
): MatchesListResponse {
return {
matches: legacyMatches.map(transformMatchListItem),
has_more: false, // Legacy API doesn't provide pagination info
next_page_time: undefined
};
}

View File

@@ -11,7 +11,8 @@
// Transform API matches to display format
const featuredMatches = data.featuredMatches.map((match) => ({
id: match.match_id.toString(),
map: match.map,
map: match.map || 'unknown',
mapDisplay: match.map ? match.map.replace('de_', '').toUpperCase() : 'UNKNOWN',
scoreT: match.score_team_a,
scoreCT: match.score_team_b,
date: new Date(match.date).toLocaleString(),
@@ -92,9 +93,7 @@
class="relative h-48 overflow-hidden rounded-t-md bg-gradient-to-br from-base-300 to-base-100"
>
<div class="absolute inset-0 flex items-center justify-center">
<span class="text-6xl font-bold text-base-content/10"
>{match.map.replace('de_', '').toUpperCase()}</span
>
<span class="text-6xl font-bold text-base-content/10">{match.mapDisplay}</span>
</div>
<div class="absolute bottom-4 left-4">
<Badge variant="default">{match.map}</Badge>

View File

@@ -14,7 +14,7 @@ export const load: PageLoad = async ({ parent }) => {
const matchesData = await api.matches.getMatches({ limit: 6 });
return {
featuredMatches: matchesData.matches,
featuredMatches: matchesData.matches.slice(0, 6), // Ensure max 6 matches
meta: {
title: 'CS2.WTF - Statistics for CS2 Matchmaking',
description:
@@ -23,7 +23,10 @@ export const load: PageLoad = async ({ parent }) => {
};
} catch (error) {
// Log error but don't fail the page load
console.error('Failed to load featured matches:', error instanceof Error ? error.message : String(error));
console.error(
'Failed to load featured matches:',
error instanceof Error ? error.message : String(error)
);
// Return empty data - page will show without featured matches
return {