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:
@@ -1,12 +1,12 @@
|
|||||||
import { apiClient } from './client';
|
import { apiClient } from './client';
|
||||||
import {
|
import {
|
||||||
parseMatch,
|
parseMatch,
|
||||||
parseMatchesList,
|
|
||||||
parseMatchRounds,
|
parseMatchRounds,
|
||||||
parseMatchWeapons,
|
parseMatchWeapons,
|
||||||
parseMatchChat,
|
parseMatchChat,
|
||||||
parseMatchParseResponse
|
parseMatchParseResponse
|
||||||
} from '$lib/schemas';
|
} from '$lib/schemas';
|
||||||
|
import { transformMatchesListResponse, type LegacyMatchListItem } from './transformers';
|
||||||
import type {
|
import type {
|
||||||
Match,
|
Match,
|
||||||
MatchesListResponse,
|
MatchesListResponse,
|
||||||
@@ -94,7 +94,8 @@ export const matchesAPI = {
|
|||||||
async getMatches(params?: MatchesQueryParams): Promise<MatchesListResponse> {
|
async getMatches(params?: MatchesQueryParams): Promise<MatchesListResponse> {
|
||||||
const url = params?.before_time ? `/matches/next/${params.before_time}` : '/matches';
|
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: {
|
params: {
|
||||||
limit: params?.limit,
|
limit: params?.limit,
|
||||||
map: params?.map,
|
map: params?.map,
|
||||||
@@ -102,8 +103,8 @@ export const matchesAPI = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate with Zod schema
|
// Transform legacy API response to new format
|
||||||
return parseMatchesList(data);
|
return transformMatchesListResponse(data);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,7 +114,8 @@ export const matchesAPI = {
|
|||||||
*/
|
*/
|
||||||
async searchMatches(params?: MatchesQueryParams): Promise<MatchesListResponse> {
|
async searchMatches(params?: MatchesQueryParams): Promise<MatchesListResponse> {
|
||||||
const url = '/matches';
|
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: {
|
params: {
|
||||||
limit: params?.limit || 20,
|
limit: params?.limit || 20,
|
||||||
map: params?.map,
|
map: params?.map,
|
||||||
@@ -122,8 +124,8 @@ export const matchesAPI = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate with Zod schema
|
// Transform legacy API response to new format
|
||||||
return parseMatchesList(data);
|
return transformMatchesListResponse(data);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
51
src/lib/api/transformers.ts
Normal file
51
src/lib/api/transformers.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
// Transform API matches to display format
|
// Transform API matches to display format
|
||||||
const featuredMatches = data.featuredMatches.map((match) => ({
|
const featuredMatches = data.featuredMatches.map((match) => ({
|
||||||
id: match.match_id.toString(),
|
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,
|
scoreT: match.score_team_a,
|
||||||
scoreCT: match.score_team_b,
|
scoreCT: match.score_team_b,
|
||||||
date: new Date(match.date).toLocaleString(),
|
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"
|
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">
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
<span class="text-6xl font-bold text-base-content/10"
|
<span class="text-6xl font-bold text-base-content/10">{match.mapDisplay}</span>
|
||||||
>{match.map.replace('de_', '').toUpperCase()}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-4 left-4">
|
<div class="absolute bottom-4 left-4">
|
||||||
<Badge variant="default">{match.map}</Badge>
|
<Badge variant="default">{match.map}</Badge>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const load: PageLoad = async ({ parent }) => {
|
|||||||
const matchesData = await api.matches.getMatches({ limit: 6 });
|
const matchesData = await api.matches.getMatches({ limit: 6 });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
featuredMatches: matchesData.matches,
|
featuredMatches: matchesData.matches.slice(0, 6), // Ensure max 6 matches
|
||||||
meta: {
|
meta: {
|
||||||
title: 'CS2.WTF - Statistics for CS2 Matchmaking',
|
title: 'CS2.WTF - Statistics for CS2 Matchmaking',
|
||||||
description:
|
description:
|
||||||
@@ -23,7 +23,10 @@ export const load: PageLoad = async ({ parent }) => {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log error but don't fail the page load
|
// 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 empty data - page will show without featured matches
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user