forked from CSGOWTF/csgowtf
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>
186 lines
4.4 KiB
TypeScript
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);
|
|
})
|
|
];
|