diff --git a/src/lib/api/matches.ts b/src/lib/api/matches.ts index c3da2ee..f0bbd0f 100644 --- a/src/lib/api/matches.ts +++ b/src/lib/api/matches.ts @@ -1,12 +1,16 @@ import { apiClient } from './client'; import { - parseMatch, parseMatchRounds, parseMatchWeapons, parseMatchChat, parseMatchParseResponse } from '$lib/schemas'; -import { transformMatchesListResponse, type LegacyMatchListItem } from './transformers'; +import { + transformMatchesListResponse, + transformMatchDetail, + type LegacyMatchListItem, + type LegacyMatchDetail +} from './transformers'; import type { Match, MatchesListResponse, @@ -36,15 +40,16 @@ export const matchesAPI = { /** * Get match details with player statistics - * @param matchId - Match ID (uint64) + * @param matchId - Match ID (uint64 as string) * @returns Complete match data */ - async getMatch(matchId: string | number): Promise { + async getMatch(matchId: string): Promise { const url = `/match/${matchId}`; - const data = await apiClient.get(url); + // API returns legacy format + const data = await apiClient.get(url); - // Validate with Zod schema - return parseMatch(data); + // Transform legacy API response to new format + return transformMatchDetail(data); }, /** diff --git a/src/lib/api/players.ts b/src/lib/api/players.ts index bcd00c5..8979f77 100644 --- a/src/lib/api/players.ts +++ b/src/lib/api/players.ts @@ -8,11 +8,11 @@ import type { Player, PlayerMeta, TrackPlayerResponse } from '$lib/types'; export const playersAPI = { /** * Get player profile with match history - * @param steamId - Steam ID (uint64) + * @param steamId - Steam ID (uint64 as string to preserve precision) * @param beforeTime - Optional Unix timestamp for pagination * @returns Player profile with recent matches */ - async getPlayer(steamId: string | number, beforeTime?: number): Promise { + async getPlayer(steamId: string, beforeTime?: number): Promise { const url = beforeTime ? `/player/${steamId}/next/${beforeTime}` : `/player/${steamId}`; const data = await apiClient.get(url); @@ -22,11 +22,11 @@ export const playersAPI = { /** * Get lightweight player metadata - * @param steamId - Steam ID + * @param steamId - Steam ID (uint64 as string to preserve precision) * @param limit - Number of recent matches to include (default: 10) * @returns Player metadata */ - async getPlayerMeta(steamId: string | number, limit = 10): Promise { + async getPlayerMeta(steamId: string, limit = 10): Promise { const url = `/player/${steamId}/meta/${limit}`; const data = await apiClient.get(url); @@ -36,21 +36,21 @@ export const playersAPI = { /** * Add player to tracking system - * @param steamId - Steam ID + * @param steamId - Steam ID (uint64 as string to preserve precision) * @param authCode - Steam authentication code * @returns Success response */ - async trackPlayer(steamId: string | number, authCode: string): Promise { + async trackPlayer(steamId: string, authCode: string): Promise { const url = `/player/${steamId}/track`; return apiClient.post(url, { auth_code: authCode }); }, /** * Remove player from tracking system - * @param steamId - Steam ID + * @param steamId - Steam ID (uint64 as string to preserve precision) * @returns Success response */ - async untrackPlayer(steamId: string | number): Promise { + async untrackPlayer(steamId: string): Promise { const url = `/player/${steamId}/track`; return apiClient.delete(url); }, diff --git a/src/lib/api/transformers.ts b/src/lib/api/transformers.ts index 5dcf1c7..f2fe933 100644 --- a/src/lib/api/transformers.ts +++ b/src/lib/api/transformers.ts @@ -3,7 +3,7 @@ * Converts legacy CSGO:WTF API responses to the new CS2.WTF format */ -import type { MatchListItem, MatchesListResponse } from '$lib/types'; +import type { MatchListItem, MatchesListResponse, Match, MatchPlayer } from '$lib/types'; /** * Legacy API match format (from api.csgow.tf) @@ -21,12 +21,71 @@ export interface LegacyMatchListItem { game_ban: boolean; } +/** + * Legacy API match detail format + */ +export interface LegacyMatchDetail { + match_id: string; + share_code?: 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; + stats?: LegacyPlayerStats[]; +} + +/** + * Legacy player stats format + */ +export interface LegacyPlayerStats { + team_id: number; + kills: number; + deaths: number; + assists: number; + headshot: number; + mvp: number; + score: number; + player: { + steamid64: string; + name: string; + avatar: string; + vac: boolean; + game_ban: boolean; + vanity_url?: string; + }; + rank: Record; + multi_kills?: { + duo?: number; + triple?: number; + quad?: number; + ace?: number; + }; + dmg?: Record; + flash?: { + duration?: { + self?: number; + team?: number; + enemy?: number; + }; + total?: { + self?: number; + team?: number; + enemy?: number; + }; + }; +} + /** * Transform legacy match list item to new format */ export function transformMatchListItem(legacy: LegacyMatchListItem): MatchListItem { return { - match_id: Number(legacy.match_id), + match_id: legacy.match_id, // Keep as string to preserve uint64 precision 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], @@ -49,3 +108,56 @@ export function transformMatchesListResponse( next_page_time: undefined }; } + +/** + * Transform legacy player stats to new format + */ +export function transformPlayerStats(legacy: LegacyPlayerStats): MatchPlayer { + return { + id: legacy.player.steamid64, + name: legacy.player.name, + avatar: `https://avatars.steamstatic.com/${legacy.player.avatar}_full.jpg`, + team_id: legacy.team_id, + kills: legacy.kills, + deaths: legacy.deaths, + assists: legacy.assists, + headshot: legacy.headshot, + mvp: legacy.mvp, + score: legacy.score, + kast: 0, // Not provided by legacy API + // Multi-kills: map legacy names to new format + mk_2: legacy.multi_kills?.duo, + mk_3: legacy.multi_kills?.triple, + mk_4: legacy.multi_kills?.quad, + mk_5: legacy.multi_kills?.ace, + // Flash stats + flash_duration_self: legacy.flash?.duration?.self, + flash_duration_team: legacy.flash?.duration?.team, + flash_duration_enemy: legacy.flash?.duration?.enemy, + flash_total_self: legacy.flash?.total?.self, + flash_total_team: legacy.flash?.total?.team, + flash_total_enemy: legacy.flash?.total?.enemy + }; +} + +/** + * Transform legacy match detail to new format + */ +export function transformMatchDetail(legacy: LegacyMatchDetail): Match { + return { + match_id: legacy.match_id, + share_code: legacy.share_code || undefined, + map: legacy.map || 'unknown', + date: new Date(legacy.date * 1000).toISOString(), + score_team_a: legacy.score[0], + score_team_b: legacy.score[1], + duration: legacy.duration, + match_result: legacy.match_result, + max_rounds: legacy.max_rounds, + demo_parsed: legacy.parsed, + vac_present: legacy.vac, + gameban_present: legacy.game_ban, + tick_rate: 64, // Default to 64, not provided by API + players: legacy.stats?.map(transformPlayerStats) + }; +} diff --git a/src/lib/schemas/api.schema.ts b/src/lib/schemas/api.schema.ts index 924a20c..60020a8 100644 --- a/src/lib/schemas/api.schema.ts +++ b/src/lib/schemas/api.schema.ts @@ -23,7 +23,7 @@ export const apiResponseSchema = (dataSchema: T) => /** MatchParseResponse schema */ export const matchParseResponseSchema = z.object({ - match_id: z.number().positive(), + match_id: z.string().min(1), // uint64 as string to preserve precision status: z.enum(['parsing', 'queued', 'completed', 'error']), message: z.string(), estimated_time: z.number().int().positive().optional() @@ -31,7 +31,7 @@ export const matchParseResponseSchema = z.object({ /** MatchParseStatus schema */ export const matchParseStatusSchema = z.object({ - match_id: z.number().positive(), + match_id: z.string().min(1), // uint64 as string to preserve precision status: z.enum(['pending', 'parsing', 'completed', 'error']), progress: z.number().int().min(0).max(100).optional(), error_message: z.string().optional() diff --git a/src/lib/schemas/match.schema.ts b/src/lib/schemas/match.schema.ts index 39a06d1..a2c1821 100644 --- a/src/lib/schemas/match.schema.ts +++ b/src/lib/schemas/match.schema.ts @@ -7,7 +7,7 @@ import { z } from 'zod'; /** MatchPlayer schema */ export const matchPlayerSchema = z.object({ - id: z.number().positive(), + id: z.string().min(1), // Steam ID uint64 as string to preserve precision name: z.string().min(1), avatar: z.string().url(), team_id: z.number().int().min(2).max(3), // 2 = T, 3 = CT @@ -59,10 +59,11 @@ export const matchPlayerSchema = z.object({ /** Match schema */ export const matchSchema = z.object({ - match_id: z.number().positive(), + match_id: z.string().min(1), // uint64 as string to preserve precision share_code: z .string() - .regex(/^CSGO-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/), + .regex(/^(CSGO-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5})?$/) + .optional(), map: z.string().min(1), date: z.string().datetime(), score_team_a: z.number().int().nonnegative(), @@ -79,7 +80,7 @@ export const matchSchema = z.object({ /** MatchListItem schema */ export const matchListItemSchema = z.object({ - match_id: z.number().positive(), + match_id: z.string().min(1), // uint64 as string to preserve precision map: z.string().min(1), date: z.string().datetime(), score_team_a: z.number().int().nonnegative(), diff --git a/src/lib/schemas/player.schema.ts b/src/lib/schemas/player.schema.ts index 6526ecc..e75da75 100644 --- a/src/lib/schemas/player.schema.ts +++ b/src/lib/schemas/player.schema.ts @@ -7,7 +7,7 @@ import { matchSchema, matchPlayerSchema } from './match.schema'; /** Player schema */ export const playerSchema = z.object({ - id: z.number().positive(), + id: z.string().min(1), // Steam ID uint64 as string to preserve precision name: z.string().min(1), avatar: z.string().url(), vanity_url: z.string().optional(), diff --git a/src/lib/types/Match.ts b/src/lib/types/Match.ts index 957aac8..9207139 100644 --- a/src/lib/types/Match.ts +++ b/src/lib/types/Match.ts @@ -3,11 +3,11 @@ * Represents a complete CS2 match with metadata and optional player stats */ export interface Match { - /** Unique match identifier (uint64) */ - match_id: number; + /** Unique match identifier (uint64 as string to preserve precision) */ + match_id: string; - /** CS:GO/CS2 share code */ - share_code: string; + /** CS:GO/CS2 share code (optional, may not always be available) */ + share_code?: string; /** Map name (e.g., "de_inferno") */ map: string; @@ -50,7 +50,7 @@ export interface Match { * Minimal match information for lists */ export interface MatchListItem { - match_id: number; + match_id: string; map: string; date: string; score_team_a: number; @@ -65,8 +65,8 @@ export interface MatchListItem { * Player performance data for a specific match */ export interface MatchPlayer { - /** Player Steam ID */ - id: number; + /** Player Steam ID (uint64 as string to preserve precision) */ + id: string; /** Player display name */ name: string; diff --git a/src/lib/types/Player.ts b/src/lib/types/Player.ts index 2f34440..611e3f8 100644 --- a/src/lib/types/Player.ts +++ b/src/lib/types/Player.ts @@ -5,8 +5,8 @@ import type { Match, MatchPlayer } from './Match'; * Represents a Steam user with CS2 statistics */ export interface Player { - /** Steam ID (uint64) */ - id: number; + /** Steam ID (uint64 as string to preserve precision) */ + id: string; /** Steam display name */ name: string; diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts index 29027d9..892373a 100644 --- a/src/lib/types/api.ts +++ b/src/lib/types/api.ts @@ -28,7 +28,7 @@ export interface APIResponse { * Match parse response */ export interface MatchParseResponse { - match_id: number; + match_id: string; // uint64 as string to preserve precision status: 'parsing' | 'queued' | 'completed' | 'error'; message: string; estimated_time?: number; // seconds @@ -38,7 +38,7 @@ export interface MatchParseResponse { * Match parse status */ export interface MatchParseStatus { - match_id: number; + match_id: string; // uint64 as string to preserve precision status: 'pending' | 'parsing' | 'completed' | 'error'; progress?: number; // 0-100 error_message?: string; @@ -60,7 +60,7 @@ export interface MatchesListResponse { export interface MatchesQueryParams { limit?: number; // 1-100 map?: string; - player_id?: number; + player_id?: string; // Steam ID uint64 as string before_time?: number; // Unix timestamp for pagination } diff --git a/src/routes/match/[id]/+layout.ts b/src/routes/match/[id]/+layout.ts index e097d7f..4f89f48 100644 --- a/src/routes/match/[id]/+layout.ts +++ b/src/routes/match/[id]/+layout.ts @@ -3,9 +3,9 @@ import { api } from '$lib/api'; import type { LayoutLoad } from './$types'; export const load: LayoutLoad = async ({ params }) => { - const matchId = Number(params.id); + const matchId = params.id; // Keep as string to preserve uint64 precision - if (isNaN(matchId) || matchId <= 0) { + if (!matchId || matchId.trim() === '') { throw error(400, 'Invalid match ID'); } diff --git a/src/routes/player/[id]/+page.ts b/src/routes/player/[id]/+page.ts index d863f72..f3fccf5 100644 --- a/src/routes/player/[id]/+page.ts +++ b/src/routes/player/[id]/+page.ts @@ -3,9 +3,9 @@ import { api } from '$lib/api'; import type { PageLoad } from './$types'; export const load: PageLoad = async ({ params }) => { - const playerId = Number(params.id); + const playerId = params.id; // Keep as string to preserve uint64 precision - if (isNaN(playerId) || playerId <= 0) { + if (!playerId || playerId.trim() === '') { throw error(400, 'Invalid player ID'); }