Files
csgowtf/src/mocks/handlers/matches.ts
vikingowl 8f3b652740 feat: Implement Phase 1 critical features and fix API integration
This commit completes the first phase of feature parity implementation and
resolves all API integration issues to match the backend API format.

## API Integration Fixes

- Remove all hardcoded default values from transformers (tick_rate, kast, player_count, steam_updated)
- Update TypeScript types to make fields optional where backend doesn't guarantee them
- Update Zod schemas to validate optional fields correctly
- Fix mock data to match real API response format (plain arrays, not wrapped objects)
- Update UI components to handle undefined values with proper fallbacks
- Add comprehensive API documentation for Match and Player endpoints

## Phase 1 Features Implemented (3/6)

### 1. Player Tracking System 
- Created TrackPlayerModal.svelte with auth code input
- Integrated track/untrack player API endpoints
- Added UI for providing optional share code
- Displays tracked status on player profiles
- Full validation and error handling

### 2. Share Code Parsing 
- Created ShareCodeInput.svelte component
- Added to matches page for easy match submission
- Real-time validation of share code format
- Parse status feedback with loading states
- Auto-redirect to match page on success

### 3. VAC/Game Ban Status 
- Added VAC and game ban count/date fields to Player type
- Display status badges on player profile pages
- Show ban count and date when available
- Visual indicators using DaisyUI badge components

## Component Improvements

- Modal.svelte: Added Svelte 5 Snippet types, actions slot support
- ThemeToggle.svelte: Removed deprecated svelte:component usage
- Tooltip.svelte: Fixed type safety with Snippet type
- All new components follow Svelte 5 runes pattern ($state, $derived, $bindable)

## Type Safety & Linting

- Fixed all ESLint errors (any types → proper types)
- Fixed form label accessibility issues
- Replaced error: any with error: unknown + proper type guards
- Added Snippet type imports where needed
- Updated all catch blocks to use instanceof Error checks

## Static Assets

- Migrated all files from public/ to static/ directory per SvelteKit best practices
- Moved 200+ map icons, screenshots, and other assets
- Updated all import paths to use /images/ (served from static/)

## Documentation

- Created IMPLEMENTATION_STATUS.md tracking all 15 missing features
- Updated API.md with optional field annotations
- Created MATCHES_API.md with comprehensive endpoint documentation
- Added inline comments marking optional vs required fields

## Testing

- Updated mock fixtures to remove default values
- Fixed mock handlers to return plain arrays like real API
- Ensured all components handle undefined gracefully

## Remaining Phase 1 Tasks

- [ ] Add VAC status column to match scoreboard
- [ ] Create weapons statistics tab for matches
- [ ] Implement recently visited players on home page

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-12 19:31:18 +01:00

186 lines
4.4 KiB
TypeScript

import { http, HttpResponse, delay } from 'msw';
import { mockMatches, mockMatchListItems, getMockMatch } from '../fixtures';
import type {
MatchParseResponse,
MatchRoundsResponse,
MatchWeaponsResponse,
MatchChatResponse
} from '$lib/types';
/**
* MSW handlers for Match API endpoints
*/
const API_BASE_URL = 'http://localhost:8000';
export const matchesHandlers = [
// GET /match/parse/:sharecode
http.get(`${API_BASE_URL}/match/parse/:sharecode`, async () => {
// Simulate parsing delay
await delay(500);
const response: MatchParseResponse = {
match_id: '358948771684207',
status: 'parsing',
message: 'Demo download and parsing initiated',
estimated_time: 120
};
return HttpResponse.json(response);
}),
// GET /match/:id
http.get(`${API_BASE_URL}/match/:id`, ({ params }) => {
const { id } = params;
const matchId = String(id);
const match = getMockMatch(matchId) || mockMatches[0];
return HttpResponse.json(match);
}),
// GET /match/:id/weapons
http.get(`${API_BASE_URL}/match/:id/weapons`, ({ params }) => {
const { id } = params;
const matchId = Number(id);
const response: MatchWeaponsResponse = {
match_id: matchId,
weapons: [
{
player_id: 765611980123456,
weapon_stats: [
{
eq_type: 17,
weapon_name: 'AK-47',
kills: 12,
damage: 1450,
hits: 48,
hit_groups: {
head: 8,
chest: 25,
stomach: 8,
left_arm: 3,
right_arm: 2,
left_leg: 1,
right_leg: 1
},
headshot_pct: 16.7
}
]
}
]
};
return HttpResponse.json(response);
}),
// GET /match/:id/rounds
http.get(`${API_BASE_URL}/match/:id/rounds`, ({ params }) => {
const { id } = params;
const matchId = Number(id);
const winReasons = ['elimination', 'bomb_defused', 'bomb_exploded'];
const response: MatchRoundsResponse = {
match_id: matchId,
rounds: Array.from({ length: 23 }, (_, i) => ({
round: i + 1,
winner: i % 2 === 0 ? 2 : 3,
win_reason: winReasons[i % 3] || 'elimination',
players: [
{
round: i + 1,
player_id: 765611980123456,
bank: 800 + i * 1000,
equipment: 650 + i * 500,
spent: 650 + i * 500,
kills_in_round: i % 3,
damage_in_round: 100 + i * 20
}
]
}))
};
return HttpResponse.json(response);
}),
// GET /match/:id/chat
http.get(`${API_BASE_URL}/match/:id/chat`, ({ params }) => {
const { id } = params;
const matchId = Number(id);
const response: MatchChatResponse = {
match_id: matchId,
messages: [
{
player_id: 765611980123456,
player_name: 'Player1',
message: 'nice shot!',
tick: 15840,
round: 8,
all_chat: true,
timestamp: '2024-11-01T19:12:34Z'
},
{
player_id: 765611980876543,
player_name: 'Player2',
message: 'thanks',
tick: 15920,
round: 8,
all_chat: true,
timestamp: '2024-11-01T19:12:38Z'
},
{
player_id: 765611980111111,
player_name: 'Player3',
message: 'rush b no stop',
tick: 18400,
round: 9,
all_chat: false,
timestamp: '2024-11-01T19:14:12Z'
}
]
};
return HttpResponse.json(response);
}),
// GET /matches
http.get(`${API_BASE_URL}/matches`, ({ request }) => {
const url = new URL(request.url);
const limit = Number(url.searchParams.get('limit')) || 50;
const map = url.searchParams.get('map');
const playerId = url.searchParams.get('player_id');
let matches = [...mockMatchListItems];
// Apply filters
if (map) {
matches = matches.filter((m) => m.map === map);
}
if (playerId) {
// In a real scenario, filter by player participation
matches = matches.slice(0, Math.ceil(matches.length / 2));
}
// NOTE: The real API returns a plain array, not a MatchesListResponse object
// The transformation to MatchesListResponse happens in the API client
const matchArray = matches.slice(0, limit);
return HttpResponse.json(matchArray);
}),
// GET /matches/next/:time
http.get(`${API_BASE_URL}/matches/next/:time`, ({ request }) => {
const url = new URL(request.url);
const limit = Number(url.searchParams.get('limit')) || 50;
// Return older matches for pagination
// NOTE: The real API returns a plain array, not a MatchesListResponse object
const matchArray = mockMatchListItems.slice(limit, limit * 2);
return HttpResponse.json(matchArray);
})
];