fix: Fix match detail data loading and add API transformation layer
- Update Zod schemas to match raw API response formats - Create transformation layer (rounds, weapons, chat) to convert raw API to structured format - Add player name mapping in transformers for better UX - Fix Svelte 5 reactivity issues in chat page (replace $effect with $derived) - Fix Chart.js compatibility with Svelte 5 state proxies using JSON serialization - Add economy advantage chart with halftime perspective flip (WIP) - Remove stray comment from details page - Update layout to load match data first, then pass to API methods 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,9 @@ import {
|
||||
type LegacyMatchListItem,
|
||||
type LegacyMatchDetail
|
||||
} from './transformers';
|
||||
import { transformRoundsResponse } from './transformers/roundsTransformer';
|
||||
import { transformWeaponsResponse } from './transformers/weaponsTransformer';
|
||||
import { transformChatResponse } from './transformers/chatTransformer';
|
||||
import type {
|
||||
Match,
|
||||
MatchesListResponse,
|
||||
@@ -55,10 +58,11 @@ export const matchesAPI = {
|
||||
/**
|
||||
* Get match weapons statistics
|
||||
* @param matchId - Match ID
|
||||
* @param match - Optional match data for player name mapping
|
||||
* @returns Weapon statistics for all players
|
||||
* @throws Error if data is invalid or demo not parsed yet
|
||||
*/
|
||||
async getMatchWeapons(matchId: string | number): Promise<MatchWeaponsResponse> {
|
||||
async getMatchWeapons(matchId: string | number, match?: Match): Promise<MatchWeaponsResponse> {
|
||||
const url = `/match/${matchId}/weapons`;
|
||||
const data = await apiClient.get<unknown>(url);
|
||||
|
||||
@@ -71,16 +75,18 @@ export const matchesAPI = {
|
||||
throw new Error('Demo not parsed yet or invalid response format');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
// Transform raw API response to structured format
|
||||
return transformWeaponsResponse(result.data, String(matchId), match);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get match round-by-round statistics
|
||||
* @param matchId - Match ID
|
||||
* @param match - Optional match data for player name mapping
|
||||
* @returns Round statistics and economy data
|
||||
* @throws Error if data is invalid or demo not parsed yet
|
||||
*/
|
||||
async getMatchRounds(matchId: string | number): Promise<MatchRoundsResponse> {
|
||||
async getMatchRounds(matchId: string | number, match?: Match): Promise<MatchRoundsResponse> {
|
||||
const url = `/match/${matchId}/rounds`;
|
||||
const data = await apiClient.get<unknown>(url);
|
||||
|
||||
@@ -93,16 +99,18 @@ export const matchesAPI = {
|
||||
throw new Error('Demo not parsed yet or invalid response format');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
// Transform raw API response to structured format
|
||||
return transformRoundsResponse(result.data, String(matchId), match);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get match chat messages
|
||||
* @param matchId - Match ID
|
||||
* @param match - Optional match data for player name mapping
|
||||
* @returns Chat messages from the match
|
||||
* @throws Error if data is invalid or demo not parsed yet
|
||||
*/
|
||||
async getMatchChat(matchId: string | number): Promise<MatchChatResponse> {
|
||||
async getMatchChat(matchId: string | number, match?: Match): Promise<MatchChatResponse> {
|
||||
const url = `/match/${matchId}/chat`;
|
||||
const data = await apiClient.get<unknown>(url);
|
||||
|
||||
@@ -115,7 +123,8 @@ export const matchesAPI = {
|
||||
throw new Error('Demo not parsed yet or invalid response format');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
// Transform raw API response to structured format
|
||||
return transformChatResponse(result.data, String(matchId), match);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
46
src/lib/api/transformers/chatTransformer.ts
Normal file
46
src/lib/api/transformers/chatTransformer.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { ChatAPIResponse } from '$lib/types/api/ChatAPIResponse';
|
||||
import type { MatchChatResponse, Message, Match } from '$lib/types';
|
||||
|
||||
/**
|
||||
* Transform raw chat API response into structured format
|
||||
* @param rawData - Raw API response
|
||||
* @param matchId - Match ID
|
||||
* @param match - Match data with player information
|
||||
* @returns Structured chat data
|
||||
*/
|
||||
export function transformChatResponse(
|
||||
rawData: ChatAPIResponse,
|
||||
matchId: string,
|
||||
match?: Match
|
||||
): MatchChatResponse {
|
||||
const messages: Message[] = [];
|
||||
|
||||
// Create player ID to name mapping
|
||||
const playerMap = new Map<string, string>();
|
||||
if (match?.players) {
|
||||
for (const player of match.players) {
|
||||
playerMap.set(player.id, player.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten all player messages into a single array
|
||||
for (const [playerId, playerMessages] of Object.entries(rawData)) {
|
||||
const playerName = playerMap.get(playerId) || `Player ${playerId}`;
|
||||
|
||||
for (const message of playerMessages) {
|
||||
messages.push({
|
||||
...message,
|
||||
player_id: Number(playerId),
|
||||
player_name: playerName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by tick
|
||||
messages.sort((a, b) => a.tick - b.tick);
|
||||
|
||||
return {
|
||||
match_id: matchId,
|
||||
messages
|
||||
};
|
||||
}
|
||||
60
src/lib/api/transformers/roundsTransformer.ts
Normal file
60
src/lib/api/transformers/roundsTransformer.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { RoundsAPIResponse } from '$lib/types/api/RoundsAPIResponse';
|
||||
import type { MatchRoundsResponse, RoundDetail, RoundStats, Match } from '$lib/types';
|
||||
|
||||
/**
|
||||
* Transform raw rounds API response into structured format
|
||||
* @param rawData - Raw API response
|
||||
* @param matchId - Match ID
|
||||
* @param match - Match data with player information
|
||||
* @returns Structured rounds data
|
||||
*/
|
||||
export function transformRoundsResponse(
|
||||
rawData: RoundsAPIResponse,
|
||||
matchId: string,
|
||||
match?: Match
|
||||
): MatchRoundsResponse {
|
||||
const rounds: RoundDetail[] = [];
|
||||
|
||||
// Create player ID to team mapping
|
||||
const playerTeamMap = new Map<string, number>();
|
||||
if (match?.players) {
|
||||
for (const player of match.players) {
|
||||
playerTeamMap.set(player.id, player.team_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert object keys to sorted round numbers
|
||||
const roundNumbers = Object.keys(rawData)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
for (const roundNum of roundNumbers) {
|
||||
const roundData = rawData[String(roundNum)];
|
||||
if (!roundData) continue;
|
||||
|
||||
const players: RoundStats[] = [];
|
||||
|
||||
// Convert player data
|
||||
for (const [playerId, [bank, equipment, spent]] of Object.entries(roundData)) {
|
||||
players.push({
|
||||
round: roundNum + 1, // API uses 0-indexed, we use 1-indexed
|
||||
bank,
|
||||
equipment,
|
||||
spent,
|
||||
player_id: Number(playerId)
|
||||
});
|
||||
}
|
||||
|
||||
rounds.push({
|
||||
round: roundNum + 1,
|
||||
winner: 0, // TODO: Determine winner from data if available
|
||||
win_reason: '', // TODO: Determine win reason if available
|
||||
players
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
match_id: matchId,
|
||||
rounds
|
||||
};
|
||||
}
|
||||
99
src/lib/api/transformers/weaponsTransformer.ts
Normal file
99
src/lib/api/transformers/weaponsTransformer.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { WeaponsAPIResponse } from '$lib/types/api/WeaponsAPIResponse';
|
||||
import type { MatchWeaponsResponse, PlayerWeaponStats, WeaponStats, Match } from '$lib/types';
|
||||
|
||||
/**
|
||||
* Transform raw weapons API response into structured format
|
||||
* @param rawData - Raw API response
|
||||
* @param matchId - Match ID
|
||||
* @param match - Match data with player information
|
||||
* @returns Structured weapons data
|
||||
*/
|
||||
export function transformWeaponsResponse(
|
||||
rawData: WeaponsAPIResponse,
|
||||
matchId: string,
|
||||
match?: Match
|
||||
): MatchWeaponsResponse {
|
||||
const playerWeaponsMap = new Map<
|
||||
string,
|
||||
Map<number, { damage: number; hits: number; hitGroups: number[] }>
|
||||
>();
|
||||
|
||||
// Create player ID to name mapping
|
||||
const playerMap = new Map<string, string>();
|
||||
if (match?.players) {
|
||||
for (const player of match.players) {
|
||||
playerMap.set(player.id, player.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Process all stats
|
||||
for (const roundStats of rawData.stats) {
|
||||
for (const [attackerId, victims] of Object.entries(roundStats)) {
|
||||
if (!playerWeaponsMap.has(attackerId)) {
|
||||
playerWeaponsMap.set(attackerId, new Map());
|
||||
}
|
||||
const weaponsMap = playerWeaponsMap.get(attackerId)!;
|
||||
|
||||
for (const [_, hits] of Object.entries(victims)) {
|
||||
for (const [eqType, hitGroup, damage] of hits) {
|
||||
if (!weaponsMap.has(eqType)) {
|
||||
weaponsMap.set(eqType, { damage: 0, hits: 0, hitGroups: [] });
|
||||
}
|
||||
const weaponStats = weaponsMap.get(eqType)!;
|
||||
weaponStats.damage += damage;
|
||||
weaponStats.hits++;
|
||||
weaponStats.hitGroups.push(hitGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to output format
|
||||
const weapons: PlayerWeaponStats[] = [];
|
||||
for (const [playerId, weaponsMap] of playerWeaponsMap.entries()) {
|
||||
const playerName = playerMap.get(playerId) || `Player ${playerId}`;
|
||||
const weapon_stats: WeaponStats[] = [];
|
||||
|
||||
for (const [eqType, stats] of weaponsMap.entries()) {
|
||||
const hitGroupCounts = {
|
||||
head: 0,
|
||||
chest: 0,
|
||||
stomach: 0,
|
||||
left_arm: 0,
|
||||
right_arm: 0,
|
||||
left_leg: 0,
|
||||
right_leg: 0
|
||||
};
|
||||
for (const hitGroup of stats.hitGroups) {
|
||||
if (hitGroup === 1) hitGroupCounts.head++;
|
||||
else if (hitGroup === 2) hitGroupCounts.chest++;
|
||||
else if (hitGroup === 3) hitGroupCounts.stomach++;
|
||||
else if (hitGroup === 4) hitGroupCounts.left_arm++;
|
||||
else if (hitGroup === 5) hitGroupCounts.right_arm++;
|
||||
else if (hitGroup === 6) hitGroupCounts.left_leg++;
|
||||
else if (hitGroup === 7) hitGroupCounts.right_leg++;
|
||||
}
|
||||
|
||||
weapon_stats.push({
|
||||
eq_type: eqType,
|
||||
weapon_name: rawData.equipment_map[String(eqType)] || `Weapon ${eqType}`,
|
||||
kills: 0, // TODO: Calculate kills if needed
|
||||
damage: stats.damage,
|
||||
hits: stats.hits,
|
||||
hit_groups: hitGroupCounts,
|
||||
headshot_pct: hitGroupCounts.head > 0 ? (hitGroupCounts.head / stats.hits) * 100 : 0
|
||||
});
|
||||
}
|
||||
|
||||
weapons.push({
|
||||
player_id: Number(playerId),
|
||||
player_name: playerName,
|
||||
weapon_stats
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
match_id: matchId,
|
||||
weapons
|
||||
};
|
||||
}
|
||||
@@ -43,6 +43,10 @@
|
||||
let canvas: HTMLCanvasElement;
|
||||
let chart: Chart<'bar'> | null = null;
|
||||
|
||||
// Convert Svelte 5 $state proxy to plain object for Chart.js compatibility
|
||||
// Using JSON parse/stringify to handle Svelte proxies that structuredClone can't handle
|
||||
const plainData = $derived(JSON.parse(JSON.stringify(data)));
|
||||
|
||||
const defaultOptions: ChartConfiguration<'bar'>['options'] = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
@@ -99,7 +103,7 @@
|
||||
if (ctx) {
|
||||
chart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: data,
|
||||
data: plainData,
|
||||
options: { ...defaultOptions, ...options }
|
||||
});
|
||||
}
|
||||
@@ -113,8 +117,8 @@
|
||||
|
||||
// Watch for data changes and update chart
|
||||
$effect(() => {
|
||||
if (chart) {
|
||||
chart.data = data;
|
||||
if (chart && plainData) {
|
||||
chart.data = plainData;
|
||||
chart.options = { ...defaultOptions, ...options };
|
||||
chart.update();
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
let canvas: HTMLCanvasElement;
|
||||
let chart: Chart<'line'> | null = null;
|
||||
|
||||
// Convert Svelte 5 $state proxy to plain object for Chart.js compatibility
|
||||
// Using JSON parse/stringify to handle Svelte proxies that structuredClone can't handle
|
||||
const plainData = $derived(JSON.parse(JSON.stringify(data)));
|
||||
|
||||
const defaultOptions: ChartConfiguration<'line'>['options'] = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
@@ -108,7 +112,7 @@
|
||||
if (ctx) {
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
data: plainData,
|
||||
options: { ...defaultOptions, ...options }
|
||||
});
|
||||
}
|
||||
@@ -122,8 +126,8 @@
|
||||
|
||||
// Watch for data changes and update chart
|
||||
$effect(() => {
|
||||
if (chart) {
|
||||
chart.data = data;
|
||||
if (chart && plainData) {
|
||||
chart.data = plainData;
|
||||
chart.options = { ...defaultOptions, ...options };
|
||||
chart.update();
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
let canvas: HTMLCanvasElement;
|
||||
let chart: Chart<'doughnut'> | null = null;
|
||||
|
||||
// Convert Svelte 5 $state proxy to plain object for Chart.js compatibility
|
||||
// Using JSON parse/stringify to handle Svelte proxies that structuredClone can't handle
|
||||
const plainData = $derived(JSON.parse(JSON.stringify(data)));
|
||||
|
||||
const defaultOptions: ChartConfiguration<'doughnut'>['options'] = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
@@ -74,7 +78,7 @@
|
||||
if (ctx) {
|
||||
chart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: data,
|
||||
data: plainData,
|
||||
options: { ...defaultOptions, ...options }
|
||||
});
|
||||
}
|
||||
@@ -88,8 +92,8 @@
|
||||
|
||||
// Watch for data changes and update chart
|
||||
$effect(() => {
|
||||
if (chart) {
|
||||
chart.data = data;
|
||||
if (chart && plainData) {
|
||||
chart.data = plainData;
|
||||
chart.options = { ...defaultOptions, ...options };
|
||||
chart.update();
|
||||
}
|
||||
|
||||
@@ -16,11 +16,12 @@ export const messageSchema = z.object({
|
||||
timestamp: z.string().datetime().optional()
|
||||
});
|
||||
|
||||
/** MatchChatResponse schema */
|
||||
export const matchChatResponseSchema = z.object({
|
||||
match_id: z.number().positive(),
|
||||
messages: z.array(messageSchema)
|
||||
});
|
||||
/** MatchChatResponse schema - matches actual API format */
|
||||
// API returns: { "player_id": [{ message, all_chat, tick }, ...], ... }
|
||||
export const matchChatResponseSchema = z.record(
|
||||
z.string(), // player Steam ID as string key
|
||||
z.array(messageSchema)
|
||||
);
|
||||
|
||||
/** EnrichedMessage schema (with player data) */
|
||||
export const enrichedMessageSchema = messageSchema.extend({
|
||||
|
||||
@@ -24,11 +24,19 @@ export const roundDetailSchema = z.object({
|
||||
players: z.array(roundStatsSchema)
|
||||
});
|
||||
|
||||
/** MatchRoundsResponse schema */
|
||||
export const matchRoundsResponseSchema = z.object({
|
||||
match_id: z.number().positive(),
|
||||
rounds: z.array(roundDetailSchema)
|
||||
});
|
||||
/** MatchRoundsResponse schema - matches actual API format */
|
||||
// API returns: { "0": { "player_id": [bank, equipment, spent] }, "1": {...}, ... }
|
||||
export const matchRoundsResponseSchema = z.record(
|
||||
z.string(), // round number as string key
|
||||
z.record(
|
||||
z.string(), // player Steam ID as string key
|
||||
z.tuple([
|
||||
z.number().int().nonnegative(), // bank
|
||||
z.number().int().nonnegative(), // equipment value
|
||||
z.number().int().nonnegative() // spent
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
/** TeamRoundStats schema */
|
||||
export const teamRoundStatsSchema = z.object({
|
||||
|
||||
@@ -42,10 +42,25 @@ export const playerWeaponStatsSchema = z.object({
|
||||
weapon_stats: z.array(weaponStatsSchema)
|
||||
});
|
||||
|
||||
/** MatchWeaponsResponse schema */
|
||||
/** MatchWeaponsResponse schema - matches actual API format */
|
||||
// API returns: { equipment_map: { "1": "P2000", ... }, stats: [...] }
|
||||
export const matchWeaponsResponseSchema = z.object({
|
||||
match_id: z.number().positive(),
|
||||
weapons: z.array(playerWeaponStatsSchema)
|
||||
equipment_map: z.record(z.string(), z.string()), // eq_type ID -> weapon name
|
||||
stats: z.array(
|
||||
z.record(
|
||||
z.string(), // attacker Steam ID
|
||||
z.record(
|
||||
z.string(), // victim Steam ID
|
||||
z.array(
|
||||
z.tuple([
|
||||
z.number().int().nonnegative(), // eq_type
|
||||
z.number().int().min(0).max(7), // hit_group
|
||||
z.number().int().nonnegative() // damage
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
});
|
||||
|
||||
/** Parser functions */
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface Message {
|
||||
* Match chat response
|
||||
*/
|
||||
export interface MatchChatResponse {
|
||||
match_id: number;
|
||||
match_id: string | number;
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,6 @@ export interface RoundDetail {
|
||||
* Complete match rounds response
|
||||
*/
|
||||
export interface MatchRoundsResponse {
|
||||
match_id: number;
|
||||
match_id: string | number;
|
||||
rounds: RoundDetail[];
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface WeaponStats {
|
||||
*/
|
||||
export interface PlayerWeaponStats {
|
||||
player_id: number;
|
||||
player_name?: string;
|
||||
weapon_stats: WeaponStats[];
|
||||
}
|
||||
|
||||
@@ -68,7 +69,7 @@ export interface PlayerWeaponStats {
|
||||
* Match weapons response
|
||||
*/
|
||||
export interface MatchWeaponsResponse {
|
||||
match_id: number;
|
||||
match_id: string | number;
|
||||
weapons: PlayerWeaponStats[];
|
||||
}
|
||||
|
||||
|
||||
9
src/lib/types/api/ChatAPIResponse.ts
Normal file
9
src/lib/types/api/ChatAPIResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { Message } from '../Message';
|
||||
|
||||
/**
|
||||
* Raw API response format for match chat endpoint
|
||||
* API returns: { "player_id": [{ message, all_chat, tick }, ...], ... }
|
||||
*/
|
||||
export interface ChatAPIResponse {
|
||||
[playerId: string]: Message[];
|
||||
}
|
||||
9
src/lib/types/api/RoundsAPIResponse.ts
Normal file
9
src/lib/types/api/RoundsAPIResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Raw API response format for match rounds endpoint
|
||||
* API returns: { "0": { "player_id": [bank, equipment, spent] }, "1": {...}, ... }
|
||||
*/
|
||||
export interface RoundsAPIResponse {
|
||||
[roundNumber: string]: {
|
||||
[playerId: string]: [bank: number, equipment: number, spent: number];
|
||||
};
|
||||
}
|
||||
12
src/lib/types/api/WeaponsAPIResponse.ts
Normal file
12
src/lib/types/api/WeaponsAPIResponse.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Raw API response format for match weapons endpoint
|
||||
* API returns: { equipment_map: { "1": "P2000", ... }, stats: [...] }
|
||||
*/
|
||||
export interface WeaponsAPIResponse {
|
||||
equipment_map: Record<string, string>; // eq_type ID -> weapon name
|
||||
stats: Array<{
|
||||
[attackerId: string]: {
|
||||
[victimId: string]: Array<[eqType: number, hitGroup: number, damage: number]>;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
* Central export for all CS2.WTF type definitions
|
||||
*/
|
||||
|
||||
// Raw API response types (internal format from backend)
|
||||
export type { RoundsAPIResponse } from './api/RoundsAPIResponse';
|
||||
export type { WeaponsAPIResponse } from './api/WeaponsAPIResponse';
|
||||
export type { ChatAPIResponse } from './api/ChatAPIResponse';
|
||||
|
||||
// Match types
|
||||
export type { Match, MatchListItem, MatchPlayer, MatchWithPlayers } from './Match';
|
||||
|
||||
|
||||
@@ -10,10 +10,26 @@ export const load: LayoutLoad = async ({ params }) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// First load match data (required)
|
||||
const match = await api.matches.getMatch(matchId);
|
||||
|
||||
// Then load optional data in parallel, passing match for player name mapping
|
||||
const [roundsResult, weaponsResult, chatResult] = await Promise.allSettled([
|
||||
api.matches.getMatchRounds(matchId, match),
|
||||
api.matches.getMatchWeapons(matchId, match),
|
||||
api.matches.getMatchChat(matchId, match)
|
||||
]);
|
||||
|
||||
// Optional data - return null if not available (demo not parsed yet)
|
||||
const rounds = roundsResult.status === 'fulfilled' ? roundsResult.value : null;
|
||||
const weapons = weaponsResult.status === 'fulfilled' ? weaponsResult.value : null;
|
||||
const chat = chatResult.status === 'fulfilled' ? chatResult.value : null;
|
||||
|
||||
return {
|
||||
match
|
||||
match,
|
||||
rounds,
|
||||
weapons,
|
||||
chat
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Failed to load match ${matchId}:`, err);
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
const matchId = params.id;
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
// Get all data from parent layout (already loaded upfront)
|
||||
const { rounds } = await parent();
|
||||
|
||||
try {
|
||||
// Fetch rounds data for the timeline visualization
|
||||
const rounds = await api.matches.getMatchRounds(matchId);
|
||||
|
||||
return {
|
||||
rounds
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Failed to load rounds for match ${matchId}:`, err);
|
||||
// Return empty rounds if the endpoint fails (demo might not be parsed yet)
|
||||
return {
|
||||
rounds: null
|
||||
};
|
||||
}
|
||||
return {
|
||||
rounds
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,12 +4,6 @@
|
||||
import Badge from '$lib/components/ui/Badge.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface MessagePlayer {
|
||||
id: number;
|
||||
name: string;
|
||||
team_id: number;
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const { match, chatData } = data;
|
||||
|
||||
@@ -19,23 +13,6 @@
|
||||
let showAllChat = $state(true);
|
||||
let selectedPlayer = $state<number | null>(null);
|
||||
|
||||
let messagePlayers = $state<MessagePlayer[]>([]);
|
||||
let filteredMessages = $state<NonNullable<PageData['chatData']>['messages']>([]);
|
||||
let messagesByRound = $state<Record<number, NonNullable<PageData['chatData']>['messages']>>({});
|
||||
let rounds = $state<number[]>([]);
|
||||
let totalMessages = $state(0);
|
||||
let teamChatCount = $state(0);
|
||||
let allChatCount = $state(0);
|
||||
|
||||
// Get player info for a message
|
||||
const getPlayerInfo = (playerId: number) => {
|
||||
const player = match.players?.find((p) => p.id === String(playerId));
|
||||
return {
|
||||
name: player?.name || `Player ${playerId}`,
|
||||
team_id: player?.team_id || 0
|
||||
};
|
||||
};
|
||||
|
||||
// Check if text likely needs translation (contains non-ASCII or Cyrillic characters)
|
||||
const mightNeedTranslation = (text: string): boolean => {
|
||||
// Check for Cyrillic, Chinese, Japanese, Korean, Arabic, etc.
|
||||
@@ -52,62 +29,68 @@
|
||||
window.open(translateUrl, '_blank', 'width=800,height=600,noopener,noreferrer');
|
||||
};
|
||||
|
||||
if (chatData) {
|
||||
// Get unique players who sent messages
|
||||
messagePlayers = Array.from(new Set(chatData.messages.map((m) => m.player_id)))
|
||||
.filter((playerId): playerId is number => playerId !== undefined)
|
||||
.map((playerId) => {
|
||||
const player = match.players?.find((p) => p.id === String(playerId));
|
||||
return {
|
||||
id: playerId,
|
||||
name: player?.name || `Player ${playerId}`,
|
||||
team_id: player?.team_id || 0
|
||||
};
|
||||
});
|
||||
// Get unique players who sent messages - use $derived for computed values
|
||||
const messagePlayers = $derived(
|
||||
chatData
|
||||
? Array.from(new Set(chatData.messages.map((m) => m.player_id)))
|
||||
.filter((playerId): playerId is number => playerId !== undefined)
|
||||
.map((playerId) => {
|
||||
const player = match.players?.find((p) => p.id === String(playerId));
|
||||
return {
|
||||
id: playerId,
|
||||
name: player?.name || `Player ${playerId}`,
|
||||
team_id: player?.team_id || 0
|
||||
};
|
||||
})
|
||||
: []
|
||||
);
|
||||
|
||||
// Filter messages
|
||||
const computeFilteredMessages = () => {
|
||||
return chatData.messages.filter((msg) => {
|
||||
// Chat type filter
|
||||
if (!showTeamChat && !msg.all_chat) return false;
|
||||
if (!showAllChat && msg.all_chat) return false;
|
||||
// Filter messages using $derived
|
||||
const filteredMessages = $derived(
|
||||
chatData
|
||||
? chatData.messages.filter((msg) => {
|
||||
// Chat type filter
|
||||
if (!showTeamChat && !msg.all_chat) return false;
|
||||
if (!showAllChat && msg.all_chat) return false;
|
||||
|
||||
// Player filter
|
||||
if (selectedPlayer !== null && msg.player_id !== selectedPlayer) return false;
|
||||
// Player filter
|
||||
if (selectedPlayer !== null && msg.player_id !== selectedPlayer) return false;
|
||||
|
||||
// Search filter
|
||||
if (searchQuery && !msg.message.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
// Search filter
|
||||
if (searchQuery && !msg.message.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
return true;
|
||||
})
|
||||
: []
|
||||
);
|
||||
|
||||
// Update filtered messages reactively
|
||||
$effect(() => {
|
||||
filteredMessages = computeFilteredMessages();
|
||||
|
||||
// Group messages by round
|
||||
messagesByRound = {};
|
||||
for (const msg of filteredMessages) {
|
||||
// Group messages by round using $derived
|
||||
const messagesByRound = $derived(
|
||||
filteredMessages.reduce(
|
||||
(acc, msg) => {
|
||||
const round = msg.round || 0;
|
||||
if (!messagesByRound[round]) {
|
||||
messagesByRound[round] = [];
|
||||
if (!acc[round]) {
|
||||
acc[round] = [];
|
||||
}
|
||||
messagesByRound[round].push(msg);
|
||||
}
|
||||
acc[round].push(msg);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, typeof filteredMessages>
|
||||
)
|
||||
);
|
||||
|
||||
rounds = Object.keys(messagesByRound)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
});
|
||||
const rounds = $derived(
|
||||
Object.keys(messagesByRound)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
);
|
||||
|
||||
// Stats
|
||||
totalMessages = chatData.messages.length;
|
||||
teamChatCount = chatData.messages.filter((m) => !m.all_chat).length;
|
||||
allChatCount = chatData.messages.filter((m) => m.all_chat).length;
|
||||
}
|
||||
// Stats using $derived
|
||||
const totalMessages = $derived(chatData?.messages.length || 0);
|
||||
const teamChatCount = $derived(chatData?.messages.filter((m) => !m.all_chat).length || 0);
|
||||
const allChatCount = $derived(chatData?.messages.filter((m) => m.all_chat).length || 0);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -229,17 +212,20 @@
|
||||
<!-- Messages -->
|
||||
<div class="divide-y divide-base-300">
|
||||
{#each messagesByRound[round] as message}
|
||||
{@const playerInfo = getPlayerInfo(message.player_id || 0)}
|
||||
{@const player = match.players?.find((p) => p.id === String(message.player_id))}
|
||||
{@const playerName =
|
||||
message.player_name || player?.name || `Player ${message.player_id}`}
|
||||
{@const teamId = player?.team_id || 0}
|
||||
<div class="p-4 transition-colors hover:bg-base-200/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<!-- Player Avatar/Icon -->
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full text-sm font-bold text-white"
|
||||
class:bg-terrorist={playerInfo.team_id === 2}
|
||||
class:bg-ct={playerInfo.team_id === 3}
|
||||
class:bg-base-300={playerInfo.team_id === 0}
|
||||
class:bg-terrorist={teamId === 2}
|
||||
class:bg-ct={teamId === 3}
|
||||
class:bg-base-300={teamId === 0}
|
||||
>
|
||||
{playerInfo.name.charAt(0).toUpperCase()}
|
||||
{playerName.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
|
||||
<!-- Message Content -->
|
||||
@@ -248,10 +234,10 @@
|
||||
<a
|
||||
href={`/player/${message.player_id || 0}`}
|
||||
class="font-semibold hover:underline"
|
||||
class:text-terrorist={playerInfo.team_id === 2}
|
||||
class:text-ct={playerInfo.team_id === 3}
|
||||
class:text-terrorist={teamId === 2}
|
||||
class:text-ct={teamId === 3}
|
||||
>
|
||||
{playerInfo.name}
|
||||
{playerName}
|
||||
</a>
|
||||
{#if message.all_chat}
|
||||
<Badge variant="success" size="sm">All Chat</Badge>
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent, params }) => {
|
||||
const { match } = await parent();
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
// Get all data from parent layout (already loaded upfront)
|
||||
const { match, chat } = await parent();
|
||||
|
||||
// Only load chat data if match is parsed
|
||||
if (!match.demo_parsed) {
|
||||
return {
|
||||
match,
|
||||
chatData: null,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Chat - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const chatData = await api.matches.getMatchChat(params.id);
|
||||
|
||||
return {
|
||||
match,
|
||||
chatData,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Chat - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Failed to load chat data for match ${params.id}:`, err);
|
||||
// Return null instead of throwing error
|
||||
return {
|
||||
match,
|
||||
chatData: null,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Chat - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
match,
|
||||
chatData: chat,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Chat - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -350,7 +350,6 @@
|
||||
</Card>
|
||||
|
||||
<!-- Top Performers -->
|
||||
// Top Performers
|
||||
<div class="grid gap-6 md:grid-cols-3">
|
||||
{#if sortedPlayers.length > 0 && sortedPlayers[0]}
|
||||
<!-- Most Kills -->
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent, params }) => {
|
||||
const { match } = await parent();
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
// Get all data from parent layout (already loaded upfront)
|
||||
const { match, weapons } = await parent();
|
||||
|
||||
// Only load weapons data if match is parsed
|
||||
if (!match.demo_parsed) {
|
||||
return {
|
||||
match,
|
||||
weaponsData: null,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Details - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const weaponsData = await api.matches.getMatchWeapons(params.id);
|
||||
|
||||
return {
|
||||
match,
|
||||
weaponsData,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Details - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Failed to load weapons data for match ${params.id}:`, err);
|
||||
// Return null instead of throwing error
|
||||
return {
|
||||
match,
|
||||
weaponsData: null,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Details - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
match,
|
||||
weaponsData: weapons,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Details - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
winner: number;
|
||||
teamA_buyType: string;
|
||||
teamB_buyType: string;
|
||||
economyAdvantage: number; // Cumulative economy differential (teamA - teamB)
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const { match, roundsData } = data;
|
||||
|
||||
// Get unique team IDs dynamically
|
||||
const uniqueTeamIds = match.players ? [...new Set(match.players.map((p) => p.team_id))] : [];
|
||||
const firstTeamId = uniqueTeamIds[0] ?? 2;
|
||||
const secondTeamId = uniqueTeamIds[1] ?? 3;
|
||||
// Team IDs - Terrorists are always team_id 2, Counter-Terrorists are always team_id 3
|
||||
const tTeamId = 2;
|
||||
const ctTeamId = 3;
|
||||
|
||||
// Only process if rounds data exists
|
||||
let teamEconomy = $state<TeamEconomy[]>([]);
|
||||
@@ -40,36 +40,46 @@
|
||||
tension?: number;
|
||||
}>;
|
||||
} | null>(null);
|
||||
let economyAdvantageChartData = $state<{
|
||||
labels: string[];
|
||||
datasets: Array<{
|
||||
label: string;
|
||||
data: number[];
|
||||
borderColor?: string;
|
||||
backgroundColor?: string;
|
||||
fill?: boolean;
|
||||
tension?: number;
|
||||
}>;
|
||||
} | null>(null);
|
||||
let totalRounds = $state(0);
|
||||
let teamA_fullBuys = $state(0);
|
||||
let teamB_fullBuys = $state(0);
|
||||
let teamA_ecos = $state(0);
|
||||
let teamB_ecos = $state(0);
|
||||
let halfRoundIndex = $state<number>(0);
|
||||
|
||||
if (roundsData) {
|
||||
// Process rounds data to calculate team totals
|
||||
for (const roundData of roundsData.rounds) {
|
||||
const teamAPlayers = roundData.players.filter((p) => {
|
||||
const tPlayers = roundData.players.filter((p) => {
|
||||
const matchPlayer = match.players?.find((mp) => mp.id === String(p.player_id));
|
||||
return matchPlayer?.team_id === firstTeamId;
|
||||
return matchPlayer?.team_id === tTeamId;
|
||||
});
|
||||
|
||||
const teamBPlayers = roundData.players.filter((p) => {
|
||||
const ctPlayers = roundData.players.filter((p) => {
|
||||
const matchPlayer = match.players?.find((mp) => mp.id === String(p.player_id));
|
||||
return matchPlayer?.team_id === secondTeamId;
|
||||
return matchPlayer?.team_id === ctTeamId;
|
||||
});
|
||||
|
||||
const teamA_bank = teamAPlayers.reduce((sum, p) => sum + (p.bank || 0), 0);
|
||||
const teamB_bank = teamBPlayers.reduce((sum, p) => sum + (p.bank || 0), 0);
|
||||
const teamA_equipment = teamAPlayers.reduce((sum, p) => sum + (p.equipment || 0), 0);
|
||||
const teamB_equipment = teamBPlayers.reduce((sum, p) => sum + (p.equipment || 0), 0);
|
||||
const teamA_spent = teamAPlayers.reduce((sum, p) => sum + (p.spent || 0), 0);
|
||||
const teamB_spent = teamBPlayers.reduce((sum, p) => sum + (p.spent || 0), 0);
|
||||
const t_bank = tPlayers.reduce((sum, p) => sum + (p.bank || 0), 0);
|
||||
const ct_bank = ctPlayers.reduce((sum, p) => sum + (p.bank || 0), 0);
|
||||
const t_equipment = tPlayers.reduce((sum, p) => sum + (p.equipment || 0), 0);
|
||||
const ct_equipment = ctPlayers.reduce((sum, p) => sum + (p.equipment || 0), 0);
|
||||
const t_spent = tPlayers.reduce((sum, p) => sum + (p.spent || 0), 0);
|
||||
const ct_spent = ctPlayers.reduce((sum, p) => sum + (p.spent || 0), 0);
|
||||
|
||||
const avgTeamA_equipment =
|
||||
teamAPlayers.length > 0 ? teamA_equipment / teamAPlayers.length : 0;
|
||||
const avgTeamB_equipment =
|
||||
teamBPlayers.length > 0 ? teamB_equipment / teamBPlayers.length : 0;
|
||||
const avgT_equipment = tPlayers.length > 0 ? t_equipment / tPlayers.length : 0;
|
||||
const avgCT_equipment = ctPlayers.length > 0 ? ct_equipment / ctPlayers.length : 0;
|
||||
|
||||
const classifyBuyType = (avgEquipment: number): string => {
|
||||
if (avgEquipment < 1500) return 'Eco';
|
||||
@@ -78,21 +88,38 @@
|
||||
return 'Full Buy';
|
||||
};
|
||||
|
||||
// Calculate per-round economy advantage using bank + spent (like old portal)
|
||||
// Teams swap sides at halftime, so we need to account for perspective flip
|
||||
const t_totalEconomy = t_bank + t_spent;
|
||||
const ct_totalEconomy = ct_bank + ct_spent;
|
||||
|
||||
// Determine perspective based on round (teams swap at half)
|
||||
const halfPoint = 12; // MR12 format: rounds 1-12 first half, 13-24 second half
|
||||
let economyAdvantage;
|
||||
if (roundData.round <= halfPoint) {
|
||||
// First half: T - CT
|
||||
economyAdvantage = t_totalEconomy - ct_totalEconomy;
|
||||
} else {
|
||||
// Second half: CT - T (teams swapped sides)
|
||||
economyAdvantage = ct_totalEconomy - t_totalEconomy;
|
||||
}
|
||||
|
||||
teamEconomy.push({
|
||||
round: roundData.round,
|
||||
teamA_bank,
|
||||
teamB_bank,
|
||||
teamA_equipment,
|
||||
teamB_equipment,
|
||||
teamA_spent,
|
||||
teamB_spent,
|
||||
teamA_bank: t_bank,
|
||||
teamB_bank: ct_bank,
|
||||
teamA_equipment: t_equipment,
|
||||
teamB_equipment: ct_equipment,
|
||||
teamA_spent: t_spent,
|
||||
teamB_spent: ct_spent,
|
||||
winner: roundData.winner || 0,
|
||||
teamA_buyType: classifyBuyType(avgTeamA_equipment),
|
||||
teamB_buyType: classifyBuyType(avgTeamB_equipment)
|
||||
teamA_buyType: classifyBuyType(avgT_equipment),
|
||||
teamB_buyType: classifyBuyType(avgCT_equipment),
|
||||
economyAdvantage
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare chart data
|
||||
// Prepare equipment value chart data
|
||||
equipmentChartData = {
|
||||
labels: teamEconomy.map((r) => `R${r.round}`),
|
||||
datasets: [
|
||||
@@ -115,6 +142,35 @@
|
||||
]
|
||||
};
|
||||
|
||||
// Prepare economy advantage chart data
|
||||
// Positive = above 0, Negative = below 0
|
||||
halfRoundIndex = Math.floor(teamEconomy.length / 2);
|
||||
economyAdvantageChartData = {
|
||||
labels: teamEconomy.map((r) => `${r.round}`),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Advantage',
|
||||
data: teamEconomy.map((r) => (r.economyAdvantage > 0 ? r.economyAdvantage : 0)),
|
||||
borderColor: 'rgb(59, 130, 246)',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.6)',
|
||||
fill: 'origin',
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4
|
||||
},
|
||||
{
|
||||
label: 'Disadvantage',
|
||||
data: teamEconomy.map((r) => (r.economyAdvantage < 0 ? r.economyAdvantage : 0)),
|
||||
borderColor: 'rgb(249, 115, 22)',
|
||||
backgroundColor: 'rgba(249, 115, 22, 0.6)',
|
||||
fill: 'origin',
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Calculate summary stats
|
||||
totalRounds = teamEconomy.length;
|
||||
teamA_fullBuys = teamEconomy.filter((r) => r.teamA_buyType === 'Full Buy').length;
|
||||
@@ -210,6 +266,57 @@
|
||||
</Card>
|
||||
{:else}
|
||||
<div class="space-y-6">
|
||||
<!-- Economy Advantage Chart -->
|
||||
<Card padding="lg">
|
||||
<div class="mb-4">
|
||||
<h2 class="text-2xl font-bold text-base-content">Economy</h2>
|
||||
<p class="text-sm text-base-content/60">Net-worth differential (bank + spent)</p>
|
||||
</div>
|
||||
{#if economyAdvantageChartData}
|
||||
<div class="relative">
|
||||
<LineChart
|
||||
data={economyAdvantageChartData}
|
||||
height={400}
|
||||
options={{
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: (context) => {
|
||||
if (context.tick.value === 0) {
|
||||
return 'rgba(156, 163, 175, 0.5)'; // Stronger line at 0
|
||||
}
|
||||
return 'rgba(156, 163, 175, 0.1)';
|
||||
},
|
||||
lineWidth: (context) => {
|
||||
return context.tick.value === 0 ? 2 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if halfRoundIndex > 0}
|
||||
<div
|
||||
class="pointer-events-none absolute top-0 flex h-full items-center"
|
||||
style="left: {(halfRoundIndex / (teamEconomy.length || 1)) * 100}%"
|
||||
>
|
||||
<div class="h-full w-px bg-base-content/20"></div>
|
||||
<div
|
||||
class="absolute -top-1 left-1/2 -translate-x-1/2 rounded bg-base-300 px-2 py-1 text-xs font-medium text-base-content/70"
|
||||
>
|
||||
Half-Point
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="grid gap-6 md:grid-cols-3">
|
||||
<Card padding="lg">
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent, params }) => {
|
||||
const { match } = await parent();
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
// Get all data from parent layout (already loaded upfront)
|
||||
const { match, rounds } = await parent();
|
||||
|
||||
// Only load rounds data if match is parsed
|
||||
if (!match.demo_parsed) {
|
||||
return {
|
||||
match,
|
||||
roundsData: null,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Economy - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const roundsData = await api.matches.getMatchRounds(params.id);
|
||||
|
||||
return {
|
||||
match,
|
||||
roundsData,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Economy - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Failed to load rounds data for match ${params.id}:`, err);
|
||||
// Return null instead of throwing error
|
||||
return {
|
||||
match,
|
||||
roundsData: null,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Economy - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
match,
|
||||
roundsData: rounds,
|
||||
meta: {
|
||||
title: `${match.map || 'Match'} Economy - Match ${match.match_id} - CS2.WTF`
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { matchesAPI } from '$lib/api/matches';
|
||||
|
||||
export const load: PageLoad = async ({ parent, params }) => {
|
||||
const { match } = await parent();
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
// Get all data from parent layout (already loaded upfront)
|
||||
const { match, weapons } = await parent();
|
||||
|
||||
try {
|
||||
// Fetch weapons statistics for this match
|
||||
const weapons = await matchesAPI.getMatchWeapons(params.id);
|
||||
|
||||
return {
|
||||
match,
|
||||
weapons
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to load weapons data:', error);
|
||||
// Return match without weapons data on error
|
||||
return {
|
||||
match,
|
||||
weapons: { match_id: parseInt(params.id), weapons: [] }
|
||||
};
|
||||
}
|
||||
return {
|
||||
match,
|
||||
weapons: weapons || { match_id: match.match_id, weapons: [] }
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user