diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index e01cb1f..8188788 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -6,25 +6,34 @@ import { APIException } from '$lib/types'; * API Client Configuration */ const getAPIBaseURL = (): string => { - // In production builds, use the configured URL directly - if (import.meta.env.PROD) { - return import.meta.env?.VITE_API_BASE_URL || 'https://api.csgow.tf'; + const apiUrl = import.meta.env?.VITE_API_BASE_URL || 'https://api.csgow.tf'; + + // Check if we're running on the server (SSR) or in production + // On the server, we must use the actual API URL, not the proxy + if (import.meta.env.SSR || import.meta.env.PROD) { + return apiUrl; } - // In development mode, ALWAYS use the Vite proxy to avoid CORS issues + // In development mode on the client, use the Vite proxy to avoid CORS issues // The proxy will forward /api requests to VITE_API_BASE_URL - // This works regardless of whether the backend is local or remote return '/api'; }; const API_BASE_URL = getAPIBaseURL(); const API_TIMEOUT = Number(import.meta.env?.VITE_API_TIMEOUT) || 10000; -// Log the API configuration in development +// Log the API configuration if (import.meta.env.DEV) { - console.log('[API Client] Development mode - using Vite proxy'); - console.log('[API Client] Frontend requests: /api/*'); - console.log('[API Client] Proxy target:', import.meta.env?.VITE_API_BASE_URL || 'http://localhost:8000'); + if (import.meta.env.SSR) { + console.log('[API Client] SSR mode - using direct API URL:', API_BASE_URL); + } else { + console.log('[API Client] Browser mode - using Vite proxy'); + console.log('[API Client] Frontend requests: /api/*'); + console.log( + '[API Client] Proxy target:', + import.meta.env?.VITE_API_BASE_URL || 'https://api.csgow.tf' + ); + } } /** diff --git a/src/lib/components/data-display/DataTable.svelte b/src/lib/components/data-display/DataTable.svelte index 5e75ddf..5cc8e84 100644 --- a/src/lib/components/data-display/DataTable.svelte +++ b/src/lib/components/data-display/DataTable.svelte @@ -10,6 +10,7 @@ render?: (value: T[keyof T], row: T) => unknown; align?: 'left' | 'center' | 'right'; class?: string; + width?: string; // e.g., '200px', '30%', 'auto' } interface Props { @@ -19,6 +20,7 @@ striped?: boolean; hoverable?: boolean; compact?: boolean; + fixedLayout?: boolean; // Use table-layout: fixed for consistent column widths } let { @@ -27,7 +29,8 @@ class: className = '', striped = false, hoverable = true, - compact = false + compact = false, + fixedLayout = false }: Props = $props(); let sortKey = $state(null); @@ -68,7 +71,12 @@
- +
{#each columns as column} @@ -76,6 +84,7 @@ class:cursor-pointer={column.sortable} class:hover:bg-base-200={column.sortable} class="text-{column.align || 'left'} {column.class || ''}" + style={column.width ? `width: ${column.width}` : ''} onclick={() => handleSort(column)} >
{#if column.render} - {@render column.render(row[column.key], row)} + {@html column.render(row[column.key], row)} {:else} {getValue(row, column)} {/if} diff --git a/src/routes/match/[id]/+page.svelte b/src/routes/match/[id]/+page.svelte index 9d07d37..e83f6dc 100644 --- a/src/routes/match/[id]/+page.svelte +++ b/src/routes/match/[id]/+page.svelte @@ -1,26 +1,31 @@ {data.meta.title} -
- -
- -
- - Total Messages -
-
{totalMessages}
-
- - -
- - Team Chat -
-
{teamChatCount}
-
- - -
- - All Chat -
-
{allChatCount}
-
-
- - +{#if !chatData} -
-
- -

Filters

-
- -
- -
- - -
- - - - - -
- - -
-
+
+ +

Match Not Parsed

+

+ This match hasn't been parsed yet, so chat data is not available. +

+ Demo parsing required
+{:else} +
+ +
+ +
+ + Total Messages +
+
{totalMessages}
+
- - {#if filteredMessages.length === 0} + +
+ + Team Chat +
+
{teamChatCount}
+
+ + +
+ + All Chat +
+
{allChatCount}
+
+
+ + -
- -

No messages match your filters.

-
-
- {:else} - {#each rounds as round} - - -
-
-

- {round === 0 ? 'Warmup / Pre-Match' : `Round ${round}`} -

- - {messagesByRound[round].length} message{messagesByRound[round].length !== 1 ? 's' : ''} - +
+
+ +

Filters

+
+ +
+ +
+ + +
+ + + + + +
+ +
+
+ - -
- {#each messagesByRound[round] as message} - {@const playerInfo = getPlayerInfo(message.player_id)} -
-
- -
- {playerInfo.name.charAt(0).toUpperCase()} -
- - -
-
- - {playerInfo.name} - - {#if message.all_chat} - All Chat - {:else} - Team - {/if} -
-

{message.message}

-
-
-
- {/each} + + {#if filteredMessages.length === 0} + +
+ +

No messages match your filters.

- {/each} - {/if} -
+ {:else} + {#each rounds as round} + + +
+
+

+ {round === 0 ? 'Warmup / Pre-Match' : `Round ${round}`} +

+ + {messagesByRound[round].length} message{messagesByRound[round].length !== 1 + ? 's' + : ''} + +
+
+ + +
+ {#each messagesByRound[round] as message} + {@const playerInfo = getPlayerInfo(message.player_id)} +
+
+ +
+ {playerInfo.name.charAt(0).toUpperCase()} +
+ + +
+
+ + {playerInfo.name} + + {#if message.all_chat} + All Chat + {:else} + Team + {/if} +
+

{message.message}

+
+
+
+ {/each} +
+
+ {/each} + {/if} +
+{/if} diff --git a/src/routes/match/[id]/chat/+page.ts b/src/routes/match/[id]/chat/+page.ts index 1c2c124..c5e5edc 100644 --- a/src/routes/match/[id]/chat/+page.ts +++ b/src/routes/match/[id]/chat/+page.ts @@ -1,27 +1,39 @@ -import { error } from '@sveltejs/kit'; -import { matchesAPI } from '$lib/api'; +import { api } from '$lib/api'; import type { PageLoad } from './$types'; -export const load: PageLoad = async ({ params, parent }) => { - try { - // Get match data from parent layout - const { match } = await parent(); +export const load: PageLoad = async ({ parent, params }) => { + const { match } = await parent(); - // Fetch chat messages - const chatData = await matchesAPI.getMatchChat(params.id); + // 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} Chat - Match ${match.match_id} - CS2.WTF`, - description: `In-game chat log for ${match.map} match` + title: `${match.map || 'Match'} Chat - Match ${match.match_id} - CS2.WTF` } }; } catch (err) { - console.error('Failed to load chat data:', err); - throw error(500, { - message: 'Failed to load chat data' - }); + 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` + } + }; } }; diff --git a/src/routes/match/[id]/details/+page.svelte b/src/routes/match/[id]/details/+page.svelte index f7e27da..bb4fc08 100644 --- a/src/routes/match/[id]/details/+page.svelte +++ b/src/routes/match/[id]/details/+page.svelte @@ -1,5 +1,5 @@ - {data.meta.title} + Match Details - CS2.WTF -
- -
- - -

Terrorists Performance

-
-
-
Total Damage
-
{teamAStats.totalDamage.toLocaleString()}
-
-
-
Utility Damage
-
{teamAStats.totalUtilityDamage.toLocaleString()}
-
-
-
Flash Assists
-
{teamAStats.totalFlashAssists}
-
-
-
Avg KAST
-
{teamAStats.avgKAST}%
-
-
-
- - - -

Counter-Terrorists Performance

-
-
-
Total Damage
-
{teamBStats.totalDamage.toLocaleString()}
-
-
-
Utility Damage
-
{teamBStats.totalUtilityDamage.toLocaleString()}
-
-
-
Flash Assists
-
{teamBStats.totalFlashAssists}
-
-
-
Avg KAST
-
{teamBStats.avgKAST}%
-
-
-
-
- - +{#if !hasPlayerData} -
-

Multi-Kill Distribution

-

- Double kills (2K), triple kills (3K), quad kills (4K), and aces (5K) per player +

+ +

No Player Data Available

+

+ Detailed player statistics are not available for this match.

+ Player data unavailable
- +{:else} +
+ +
+ + +

Terrorists Performance

+
+
+
Total Damage
+
{teamAStats.totalDamage.toLocaleString()}
+
+
+
Utility Damage
+
{teamAStats.totalUtilityDamage.toLocaleString()}
+
+
+
Flash Assists
+
{teamAStats.totalFlashAssists}
+
+
+
Avg KAST
+
{teamAStats.avgKAST}%
+
+
+
- - -
-

Detailed Player Statistics

-

- Complete performance breakdown for all players -

+ + +

Counter-Terrorists Performance

+
+
+
Total Damage
+
{teamBStats.totalDamage.toLocaleString()}
+
+
+
Utility Damage
+
{teamBStats.totalUtilityDamage.toLocaleString()}
+
+
+
Flash Assists
+
{teamBStats.totalFlashAssists}
+
+
+
Avg KAST
+
{teamBStats.avgKAST}%
+
+
+
- -
+ + +
+

Multi-Kill Distribution

+

+ Double kills (2K), triple kills (3K), quad kills (4K), and aces (5K) per player +

+
+ +
- -
- {#if sortedPlayers.length > 0} - - -
- -

Most Kills

-
-
{sortedPlayers[0].name}
-
{sortedPlayers[0].kills}
-
- {sortedPlayers[0].deaths} deaths, {sortedPlayers[0].kd.toFixed(2)} K/D -
-
+ + +
+

Detailed Player Statistics

+

+ Complete performance breakdown for all players +

+
- - {@const bestKD = [...sortedPlayers].sort((a, b) => b.kd - a.kd)[0]} - -
- -

Best K/D Ratio

-
-
{bestKD.name}
-
{bestKD.kd.toFixed(2)}
-
- {bestKD.kills}K / {bestKD.deaths}D -
-
+ +
- - {@const bestUtility = [...sortedPlayers].sort( - (a, b) => (b.ud_he || 0) + (b.ud_flames || 0) - ((a.ud_he || 0) + (a.ud_flames || 0)) - )[0]} - -
- -

Most Utility Damage

-
-
{bestUtility.name}
-
- {((bestUtility.ud_he || 0) + (bestUtility.ud_flames || 0)).toLocaleString()} -
-
- HE: {bestUtility.ud_he || 0} | Fire: {bestUtility.ud_flames || 0} -
-
- {/if} + +
+ {#if sortedPlayers.length > 0} + + +
+ +

Most Kills

+
+
{sortedPlayers[0].name}
+
{sortedPlayers[0].kills}
+
+ {sortedPlayers[0].deaths} deaths, {sortedPlayers[0].kd.toFixed(2)} K/D +
+
+ + + {@const bestKD = [...sortedPlayers].sort((a, b) => b.kd - a.kd)[0]} + +
+ +

Best K/D Ratio

+
+
{bestKD.name}
+
{bestKD.kd.toFixed(2)}
+
+ {bestKD.kills}K / {bestKD.deaths}D +
+
+ + + {@const bestUtility = [...sortedPlayers].sort( + (a, b) => (b.ud_he || 0) + (b.ud_flames || 0) - ((a.ud_he || 0) + (a.ud_flames || 0)) + )[0]} + +
+ +

Most Utility Damage

+
+
{bestUtility.name}
+
+ {((bestUtility.ud_he || 0) + (bestUtility.ud_flames || 0)).toLocaleString()} +
+
+ HE: {bestUtility.ud_he || 0} | Fire: {bestUtility.ud_flames || 0} +
+
+ {/if} +
-
+{/if} diff --git a/src/routes/match/[id]/details/+page.ts b/src/routes/match/[id]/details/+page.ts index 8788ff8..76f4963 100644 --- a/src/routes/match/[id]/details/+page.ts +++ b/src/routes/match/[id]/details/+page.ts @@ -1,27 +1,39 @@ -import { error } from '@sveltejs/kit'; -import { matchesAPI } from '$lib/api'; +import { api } from '$lib/api'; import type { PageLoad } from './$types'; -export const load: PageLoad = async ({ params, parent }) => { - try { - // Get match data from parent layout - const { match } = await parent(); +export const load: PageLoad = async ({ parent, params }) => { + const { match } = await parent(); - // Fetch weapon statistics - const weaponsData = await matchesAPI.getMatchWeapons(params.id); + // 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} Details - Match ${match.match_id} - CS2.WTF`, - description: `Detailed player statistics and weapon breakdown for ${match.map} match` + title: `${match.map || 'Match'} Details - Match ${match.match_id} - CS2.WTF` } }; } catch (err) { - console.error('Failed to load match details:', err); - throw error(500, { - message: 'Failed to load match details' - }); + 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` + } + }; } }; diff --git a/src/routes/match/[id]/economy/+page.svelte b/src/routes/match/[id]/economy/+page.svelte index 9f0e341..6b59dbe 100644 --- a/src/routes/match/[id]/economy/+page.svelte +++ b/src/routes/match/[id]/economy/+page.svelte @@ -1,15 +1,12 @@ - - {data.meta.title} - +{#if !roundsData} + +
+ +

Match Not Parsed

+

+ This match hasn't been parsed yet, so detailed economy data is not available. +

+ Demo parsing required +
+
+{:else} +
+ +
+ +
+ + Total Rounds +
+
{totalRounds}
+
+ {match.score_team_a} - {match.score_team_b} +
+
-
- -
+ +
+ + Terrorists Buy Rounds +
+
{teamA_fullBuys}
+
{teamA_ecos} eco rounds
+
+ + +
+ + CT Buy Rounds +
+
{teamB_fullBuys}
+
{teamB_ecos} eco rounds
+
+
+ + -
- - Total Rounds -
-
{totalRounds}
-
- {match.score_team_a} - {match.score_team_b} +
+

Equipment Value Over Time

+

+ Total equipment value for each team across all rounds +

+ {#if equipmentChartData} + + {/if} - -
- - Terrorists Buy Rounds + + +
+

Round-by-Round Economy

+

+ Detailed breakdown of buy types and equipment values +

-
{teamA_fullBuys}
-
{teamA_ecos} eco rounds
+ +
+ -
- - CT Buy Rounds +

Buy Type Classification

+
+
+ Eco + < $1,500 avg equipment +
+
+ Semi-Eco + $1,500 - $2,500 avg equipment +
+
+ Force + $2,500 - $3,500 avg equipment +
+
+ Full Buy + > $3,500 avg equipment +
-
{teamB_fullBuys}
-
{teamB_ecos} eco rounds
- - - -
-

Equipment Value Over Time

-

- Total equipment value for each team across all rounds -

-
- -
- - - -
-

Round-by-Round Economy

-

- Detailed breakdown of buy types and equipment values -

-
- - -
- - - -

Buy Type Classification

-
-
- Eco - < $1,500 avg equipment -
-
- Semi-Eco - $1,500 - $2,500 avg equipment -
-
- Force - $2,500 - $3,500 avg equipment -
-
- Full Buy - > $3,500 avg equipment -
-
-
-
+{/if} diff --git a/src/routes/match/[id]/economy/+page.ts b/src/routes/match/[id]/economy/+page.ts index 2eb2b43..ec20f83 100644 --- a/src/routes/match/[id]/economy/+page.ts +++ b/src/routes/match/[id]/economy/+page.ts @@ -1,27 +1,39 @@ -import { error } from '@sveltejs/kit'; -import { matchesAPI } from '$lib/api'; +import { api } from '$lib/api'; import type { PageLoad } from './$types'; -export const load: PageLoad = async ({ params, parent }) => { - try { - // Get match data from parent layout - const { match } = await parent(); +export const load: PageLoad = async ({ parent, params }) => { + const { match } = await parent(); - // Fetch round-by-round economy data - const roundsData = await matchesAPI.getMatchRounds(params.id); + // 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} Economy - Match ${match.match_id} - CS2.WTF`, - description: `Round-by-round economy analysis for ${match.map} match` + title: `${match.map || 'Match'} Economy - Match ${match.match_id} - CS2.WTF` } }; } catch (err) { - console.error('Failed to load economy data:', err); - throw error(500, { - message: 'Failed to load economy data' - }); + 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` + } + }; } }; diff --git a/src/routes/match/[id]/flashes/+page.svelte b/src/routes/match/[id]/flashes/+page.svelte index aa421e0..fd5eef9 100644 --- a/src/routes/match/[id]/flashes/+page.svelte +++ b/src/routes/match/[id]/flashes/+page.svelte @@ -27,9 +27,14 @@ })) .sort((a, b) => b.enemies_blinded - a.enemies_blinded); + // 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; + // Separate by team - const teamAFlashStats = flashStats.filter((p) => p.team_id === 2); - const teamBFlashStats = flashStats.filter((p) => p.team_id === 3); + const teamAFlashStats = flashStats.filter((p) => p.team_id === firstTeamId); + const teamBFlashStats = flashStats.filter((p) => p.team_id === secondTeamId); // Calculate team totals const calcTeamTotals = (players: typeof flashStats) => ({ @@ -44,39 +49,44 @@ const teamATotals = calcTeamTotals(teamAFlashStats); const teamBTotals = calcTeamTotals(teamBFlashStats); - // Table columns + // Table columns with fixed widths for consistency across multiple tables const columns = [ - { key: 'name', label: 'Player', sortable: true }, + { key: 'name', label: 'Player', sortable: true, width: '200px' }, { key: 'enemies_blinded', label: 'Enemies Blinded', sortable: true, - align: 'center' as const + align: 'center' as const, + width: '150px' }, { key: 'avg_blind_duration', label: 'Avg Duration (s)', sortable: true, align: 'center' as const, - formatter: (value: string) => `${value}s` + formatter: (value: string) => `${value}s`, + width: '150px' }, { key: 'flash_assists', label: 'Flash Assists', sortable: true, - align: 'center' as const + align: 'center' as const, + width: '130px' }, { key: 'teammates_blinded', label: 'Team Flashed', sortable: true, - align: 'center' as const + align: 'center' as const, + width: '130px' }, { key: 'self_blinded', label: 'Self Flashed', sortable: true, - align: 'center' as const + align: 'center' as const, + width: '130px' } ]; @@ -159,7 +169,7 @@

- +
@@ -167,7 +177,7 @@

Terrorists - Flash Stats

- + @@ -175,7 +185,7 @@

Counter-Terrorists - Flash Stats

- + diff --git a/src/routes/matches/+page.svelte b/src/routes/matches/+page.svelte index b396b80..fd2a839 100644 --- a/src/routes/matches/+page.svelte +++ b/src/routes/matches/+page.svelte @@ -39,7 +39,7 @@ }); // Computed filtered and sorted matches - const displayMatches = $derived(() => { + const displayMatches = $derived.by(() => { let filtered = [...matches]; // Apply result filter @@ -267,15 +267,15 @@ {#if matches.length > 0 && resultFilter !== 'all'}
- Showing {displayMatches().length} of {matches.length} matches + Showing {displayMatches.length} of {matches.length} matches
{/if} - {#if displayMatches().length > 0} + {#if displayMatches.length > 0}
- {#each displayMatches() as match} + {#each displayMatches as match} {/each}
@@ -299,12 +299,12 @@
All matches loaded ({matches.length} total{resultFilter !== 'all' - ? `, ${displayMatches().length} shown` + ? `, ${displayMatches.length} shown` : ''})
{/if} - {:else if matches.length > 0 && displayMatches().length === 0} + {:else if matches.length > 0 && displayMatches.length === 0}