feat: complete Phase 3 - Domain Modeling & Data Layer

Implements comprehensive type system, runtime validation, API client,
and testing infrastructure for CS2.WTF.

TypeScript Interfaces (src/lib/types/):
- Match.ts: Match, MatchPlayer, MatchListItem types
- Player.ts: Player, PlayerMeta, PlayerProfile types
- RoundStats.ts: Round economy and performance data
- Weapon.ts: Weapon statistics with hit groups (HitGroup, WeaponType enums)
- Message.ts: Chat messages with filtering support
- api.ts: API responses, errors, and APIException class
- Complete type safety with strict null checks

Zod Schemas (src/lib/schemas/):
- Runtime validation for all data models
- Schema parsers with safe/unsafe variants
- Special handling for backend typo (looses → losses)
- Share code validation regex
- CS2-specific validations (rank 0-30000, MR12 rounds)

API Client (src/lib/api/):
- client.ts: Axios-based HTTP client with error handling
  - Request cancellation support (AbortController)
  - Automatic retry logic for transient failures
  - Timeout handling (10s default)
  - Typed APIException errors
- players.ts: Player endpoints (profile, meta, track/untrack, search)
- matches.ts: Match endpoints (parse, details, weapons, rounds, chat, search)
- Zod validation on all API responses

MSW Mock Handlers (src/mocks/):
- fixtures.ts: Comprehensive mock data for testing
- handlers/players.ts: Mock player API endpoints
- handlers/matches.ts: Mock match API endpoints
- browser.ts: Browser MSW worker for development
- server.ts: Node MSW server for Vitest tests
- Realistic responses with delays and pagination
- Safe integer IDs to avoid precision loss

Configuration:
- .env.example: Complete environment variable documentation
- src/vite-env.d.ts: Vite environment type definitions
- All strict TypeScript checks passing (0 errors, 0 warnings)

Features:
- Cancellable requests for search (prevent race conditions)
- Data normalization (backend typo handling)
- Comprehensive error types (NetworkError, Timeout, etc.)
- Share code parsing and validation
- Pagination support for players and matches
- Mock data for offline development and testing

Build Status: ✓ Production build successful
Type Check: ✓ 0 errors, 0 warnings
Lint: ✓ All checks passed
Phase 3 Completion: 100%

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-04 20:31:20 +01:00
parent 66aea51c39
commit d811efc394
26 changed files with 2444 additions and 6 deletions

View File

@@ -0,0 +1,101 @@
import { z } from 'zod';
/**
* Zod schemas for Match data models
* Provides runtime validation and type safety
*/
/** MatchPlayer schema */
export const matchPlayerSchema = z.object({
id: z.number().positive(),
name: z.string().min(1),
avatar: z.string().url(),
team_id: z.number().int().min(2).max(3), // 2 = T, 3 = CT
// Performance metrics
kills: z.number().int().nonnegative(),
deaths: z.number().int().nonnegative(),
assists: z.number().int().nonnegative(),
headshot: z.number().int().nonnegative(),
mvp: z.number().int().nonnegative(),
score: z.number().int().nonnegative(),
kast: z.number().int().min(0).max(100),
// Rank (CS2 Premier rating: 0-30000)
rank_old: z.number().int().min(0).max(30000).optional(),
rank_new: z.number().int().min(0).max(30000).optional(),
// Damage
dmg_enemy: z.number().int().nonnegative().optional(),
dmg_team: z.number().int().nonnegative().optional(),
// Multi-kills
mk_2: z.number().int().nonnegative().optional(),
mk_3: z.number().int().nonnegative().optional(),
mk_4: z.number().int().nonnegative().optional(),
mk_5: z.number().int().nonnegative().optional(),
// Utility damage
ud_he: z.number().int().nonnegative().optional(),
ud_flames: z.number().int().nonnegative().optional(),
ud_flash: z.number().int().nonnegative().optional(),
ud_smoke: z.number().int().nonnegative().optional(),
ud_decoy: z.number().int().nonnegative().optional(),
// Flash statistics
flash_assists: z.number().int().nonnegative().optional(),
flash_duration_enemy: z.number().nonnegative().optional(),
flash_duration_team: z.number().nonnegative().optional(),
flash_duration_self: z.number().nonnegative().optional(),
flash_total_enemy: z.number().int().nonnegative().optional(),
flash_total_team: z.number().int().nonnegative().optional(),
flash_total_self: z.number().int().nonnegative().optional(),
// Other
crosshair: z.string().optional(),
color: z.enum(['green', 'yellow', 'purple', 'blue', 'orange', 'grey']).optional(),
avg_ping: z.number().nonnegative().optional()
});
/** Match schema */
export const matchSchema = z.object({
match_id: z.number().positive(),
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}$/),
map: z.string().min(1),
date: z.string().datetime(),
score_team_a: z.number().int().nonnegative(),
score_team_b: z.number().int().nonnegative(),
duration: z.number().int().positive(),
match_result: z.number().int().min(0).max(2), // 0 = tie, 1 = team_a win, 2 = team_b win
max_rounds: z.number().int().positive(),
demo_parsed: z.boolean(),
vac_present: z.boolean(),
gameban_present: z.boolean(),
tick_rate: z.number().positive(),
players: z.array(matchPlayerSchema).optional()
});
/** MatchListItem schema */
export const matchListItemSchema = z.object({
match_id: z.number().positive(),
map: z.string().min(1),
date: z.string().datetime(),
score_team_a: z.number().int().nonnegative(),
score_team_b: z.number().int().nonnegative(),
duration: z.number().int().positive(),
demo_parsed: z.boolean(),
player_count: z.number().int().min(2).max(10)
});
/** Parser functions for safe data validation */
export const parseMatch = (data: unknown) => matchSchema.parse(data);
export const parseMatchSafe = (data: unknown) => matchSchema.safeParse(data);
export const parseMatchPlayer = (data: unknown) => matchPlayerSchema.parse(data);
export const parseMatchListItem = (data: unknown) => matchListItemSchema.parse(data);
/** Infer TypeScript types from schemas */
export type MatchSchema = z.infer<typeof matchSchema>;
export type MatchPlayerSchema = z.infer<typeof matchPlayerSchema>;
export type MatchListItemSchema = z.infer<typeof matchListItemSchema>;