fix: Fix match page SSR, tab errors, and table consistency

- Enable SSR for match pages by detecting server vs client context in API client
- Fix 500 errors on economy, chat, and details tabs by adding data loaders
- Handle unparsed matches gracefully with "Match Not Parsed" messages
- Fix dynamic team ID detection instead of hardcoding team IDs 2/3
- Fix DataTable component to properly render HTML in render functions
- Add fixed column widths to tables for visual consistency
- Add meta titles to all tab page loaders
- Fix Svelte 5 $derived syntax errors
- Fix ESLint errors (unused imports, any types, reactive state)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-05 00:27:47 +01:00
parent 7d8e3a6de0
commit 62bfdc8090
11 changed files with 797 additions and 591 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Trophy, Target, Flame, Zap } from 'lucide-svelte';
import { Trophy, Target, Flame, AlertCircle } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte';
import DataTable from '$lib/components/data-display/DataTable.svelte';
@@ -7,25 +7,40 @@
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
const { match, weaponsData } = data;
const { match } = data;
// Check if we have player data to display
const hasPlayerData = match.players && match.players.length > 0;
// 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;
// Calculate additional stats for players
const playersWithStats = (match.players || []).map((player) => {
const kd = player.deaths > 0 ? (player.kills / player.deaths).toFixed(2) : player.kills.toFixed(2);
const hsPercent = player.kills > 0 ? ((player.headshot / player.kills) * 100).toFixed(1) : '0.0';
const adr = player.dmg_enemy ? (player.dmg_enemy / (match.max_rounds || 24)).toFixed(1) : '0.0';
const playersWithStats = hasPlayerData
? (match.players || []).map((player) => {
const kd =
player.deaths > 0 ? (player.kills / player.deaths).toFixed(2) : player.kills.toFixed(2);
const hsPercent =
player.kills > 0 ? ((player.headshot / player.kills) * 100).toFixed(1) : '0.0';
const adr = player.dmg_enemy
? (player.dmg_enemy / (match.max_rounds || 24)).toFixed(1)
: '0.0';
return {
...player,
kd: parseFloat(kd),
hsPercent: parseFloat(hsPercent),
adr: parseFloat(adr),
totalMultiKills: (player.mk_2 || 0) + (player.mk_3 || 0) + (player.mk_4 || 0) + (player.mk_5 || 0)
};
});
return {
...player,
kd: parseFloat(kd),
hsPercent: parseFloat(hsPercent),
adr: parseFloat(adr),
totalMultiKills:
(player.mk_2 || 0) + (player.mk_3 || 0) + (player.mk_4 || 0) + (player.mk_5 || 0)
};
})
: [];
// Sort by kills descending
const sortedPlayers = playersWithStats.sort((a, b) => b.kills - a.kills);
const sortedPlayers = hasPlayerData ? playersWithStats.sort((a, b) => b.kills - a.kills) : [];
// Prepare data table columns
const detailsColumns = [
@@ -33,18 +48,52 @@
key: 'name',
label: 'Player',
sortable: true,
render: (value: string, row: any) => {
const teamClass = row.team_id === 2 ? 'text-terrorist' : 'text-ct';
render: (value: string, row: (typeof playersWithStats)[0]) => {
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
return `<a href="/player/${row.id}" class="font-medium hover:underline ${teamClass}">${value}</a>`;
}
},
{ key: 'kills', label: 'K', sortable: true, align: 'center' as const, class: 'font-mono font-semibold' },
{
key: 'kills',
label: 'K',
sortable: true,
align: 'center' as const,
class: 'font-mono font-semibold'
},
{ key: 'deaths', label: 'D', sortable: true, align: 'center' as const, class: 'font-mono' },
{ key: 'assists', label: 'A', sortable: true, align: 'center' as const, class: 'font-mono' },
{ key: 'kd', label: 'K/D', sortable: true, align: 'center' as const, class: 'font-mono', format: (v: number) => v.toFixed(2) },
{ key: 'adr', label: 'ADR', sortable: true, align: 'center' as const, class: 'font-mono', format: (v: number) => v.toFixed(1) },
{ key: 'hsPercent', label: 'HS%', sortable: true, align: 'center' as const, class: 'font-mono', format: (v: number) => `${v.toFixed(1)}%` },
{ key: 'kast', label: 'KAST%', sortable: true, align: 'center' as const, class: 'font-mono', format: (v: number) => `${v.toFixed(1)}%` },
{
key: 'kd',
label: 'K/D',
sortable: true,
align: 'center' as const,
class: 'font-mono',
format: (v: number) => v.toFixed(2)
},
{
key: 'adr',
label: 'ADR',
sortable: true,
align: 'center' as const,
class: 'font-mono',
format: (v: number) => v.toFixed(1)
},
{
key: 'hsPercent',
label: 'HS%',
sortable: true,
align: 'center' as const,
class: 'font-mono',
format: (v: number) => `${v.toFixed(1)}%`
},
{
key: 'kast',
label: 'KAST%',
sortable: true,
align: 'center' as const,
class: 'font-mono',
format: (v: number) => `${v.toFixed(1)}%`
},
{ key: 'mvp', label: 'MVP', sortable: true, align: 'center' as const, class: 'font-mono' },
{
key: 'mk_5',
@@ -86,148 +135,185 @@
};
// Calculate team totals
const teamAPlayers = playersWithStats.filter((p) => p.team_id === 2);
const teamBPlayers = playersWithStats.filter((p) => p.team_id === 3);
const teamAPlayers = hasPlayerData
? playersWithStats.filter((p) => p.team_id === firstTeamId)
: [];
const teamBPlayers = hasPlayerData
? playersWithStats.filter((p) => p.team_id === secondTeamId)
: [];
const teamAStats = {
totalDamage: teamAPlayers.reduce((sum, p) => sum + (p.dmg_enemy || 0), 0),
totalUtilityDamage: teamAPlayers.reduce((sum, p) => sum + (p.ud_he || 0) + (p.ud_flames || 0), 0),
totalFlashAssists: teamAPlayers.reduce((sum, p) => sum + (p.flash_assists || 0), 0),
avgKAST: (teamAPlayers.reduce((sum, p) => sum + (p.kast || 0), 0) / teamAPlayers.length).toFixed(1)
};
const teamAStats = hasPlayerData
? {
totalDamage: teamAPlayers.reduce((sum, p) => sum + (p.dmg_enemy || 0), 0),
totalUtilityDamage: teamAPlayers.reduce(
(sum, p) => sum + (p.ud_he || 0) + (p.ud_flames || 0),
0
),
totalFlashAssists: teamAPlayers.reduce((sum, p) => sum + (p.flash_assists || 0), 0),
avgKAST:
teamAPlayers.length > 0
? (
teamAPlayers.reduce((sum, p) => sum + (p.kast || 0), 0) / teamAPlayers.length
).toFixed(1)
: '0.0'
}
: { totalDamage: 0, totalUtilityDamage: 0, totalFlashAssists: 0, avgKAST: '0.0' };
const teamBStats = {
totalDamage: teamBPlayers.reduce((sum, p) => sum + (p.dmg_enemy || 0), 0),
totalUtilityDamage: teamBPlayers.reduce((sum, p) => sum + (p.ud_he || 0) + (p.ud_flames || 0), 0),
totalFlashAssists: teamBPlayers.reduce((sum, p) => sum + (p.flash_assists || 0), 0),
avgKAST: (teamBPlayers.reduce((sum, p) => sum + (p.kast || 0), 0) / teamBPlayers.length).toFixed(1)
};
const teamBStats = hasPlayerData
? {
totalDamage: teamBPlayers.reduce((sum, p) => sum + (p.dmg_enemy || 0), 0),
totalUtilityDamage: teamBPlayers.reduce(
(sum, p) => sum + (p.ud_he || 0) + (p.ud_flames || 0),
0
),
totalFlashAssists: teamBPlayers.reduce((sum, p) => sum + (p.flash_assists || 0), 0),
avgKAST:
teamBPlayers.length > 0
? (
teamBPlayers.reduce((sum, p) => sum + (p.kast || 0), 0) / teamBPlayers.length
).toFixed(1)
: '0.0'
}
: { totalDamage: 0, totalUtilityDamage: 0, totalFlashAssists: 0, avgKAST: '0.0' };
</script>
<svelte:head>
<title>{data.meta.title}</title>
<title>Match Details - CS2.WTF</title>
</svelte:head>
<div class="space-y-6">
<!-- Team Performance Summary -->
<div class="grid gap-6 md:grid-cols-2">
<!-- Terrorists Stats -->
<Card padding="lg">
<h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists Performance</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-base-content/60">Total Damage</div>
<div class="text-2xl font-bold">{teamAStats.totalDamage.toLocaleString()}</div>
</div>
<div>
<div class="text-base-content/60">Utility Damage</div>
<div class="text-2xl font-bold">{teamAStats.totalUtilityDamage.toLocaleString()}</div>
</div>
<div>
<div class="text-base-content/60">Flash Assists</div>
<div class="text-2xl font-bold">{teamAStats.totalFlashAssists}</div>
</div>
<div>
<div class="text-base-content/60">Avg KAST</div>
<div class="text-2xl font-bold">{teamAStats.avgKAST}%</div>
</div>
</div>
</Card>
<!-- Counter-Terrorists Stats -->
<Card padding="lg">
<h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists Performance</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-base-content/60">Total Damage</div>
<div class="text-2xl font-bold">{teamBStats.totalDamage.toLocaleString()}</div>
</div>
<div>
<div class="text-base-content/60">Utility Damage</div>
<div class="text-2xl font-bold">{teamBStats.totalUtilityDamage.toLocaleString()}</div>
</div>
<div>
<div class="text-base-content/60">Flash Assists</div>
<div class="text-2xl font-bold">{teamBStats.totalFlashAssists}</div>
</div>
<div>
<div class="text-base-content/60">Avg KAST</div>
<div class="text-2xl font-bold">{teamBStats.avgKAST}%</div>
</div>
</div>
</Card>
</div>
<!-- Multi-Kills Chart -->
{#if !hasPlayerData}
<Card padding="lg">
<div class="mb-4">
<h2 class="text-2xl font-bold text-base-content">Multi-Kill Distribution</h2>
<p class="text-sm text-base-content/60">
Double kills (2K), triple kills (3K), quad kills (4K), and aces (5K) per player
<div class="text-center">
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" />
<h2 class="mb-2 text-2xl font-bold text-base-content">No Player Data Available</h2>
<p class="mb-4 text-base-content/60">
Detailed player statistics are not available for this match.
</p>
<Badge variant="warning" size="lg">Player data unavailable</Badge>
</div>
<BarChart data={multiKillData} height={300} />
</Card>
{:else}
<div class="space-y-6">
<!-- Team Performance Summary -->
<div class="grid gap-6 md:grid-cols-2">
<!-- Terrorists Stats -->
<Card padding="lg">
<h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists Performance</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-base-content/60">Total Damage</div>
<div class="text-2xl font-bold">{teamAStats.totalDamage.toLocaleString()}</div>
</div>
<div>
<div class="text-base-content/60">Utility Damage</div>
<div class="text-2xl font-bold">{teamAStats.totalUtilityDamage.toLocaleString()}</div>
</div>
<div>
<div class="text-base-content/60">Flash Assists</div>
<div class="text-2xl font-bold">{teamAStats.totalFlashAssists}</div>
</div>
<div>
<div class="text-base-content/60">Avg KAST</div>
<div class="text-2xl font-bold">{teamAStats.avgKAST}%</div>
</div>
</div>
</Card>
<!-- Detailed Player Statistics Table -->
<Card padding="none">
<div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Detailed Player Statistics</h2>
<p class="mt-1 text-sm text-base-content/60">
Complete performance breakdown for all players
</p>
<!-- Counter-Terrorists Stats -->
<Card padding="lg">
<h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists Performance</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-base-content/60">Total Damage</div>
<div class="text-2xl font-bold">{teamBStats.totalDamage.toLocaleString()}</div>
</div>
<div>
<div class="text-base-content/60">Utility Damage</div>
<div class="text-2xl font-bold">{teamBStats.totalUtilityDamage.toLocaleString()}</div>
</div>
<div>
<div class="text-base-content/60">Flash Assists</div>
<div class="text-2xl font-bold">{teamBStats.totalFlashAssists}</div>
</div>
<div>
<div class="text-base-content/60">Avg KAST</div>
<div class="text-2xl font-bold">{teamBStats.avgKAST}%</div>
</div>
</div>
</Card>
</div>
<DataTable data={sortedPlayers} columns={detailsColumns} striped hoverable />
</Card>
<!-- Multi-Kills Chart -->
<Card padding="lg">
<div class="mb-4">
<h2 class="text-2xl font-bold text-base-content">Multi-Kill Distribution</h2>
<p class="text-sm text-base-content/60">
Double kills (2K), triple kills (3K), quad kills (4K), and aces (5K) per player
</p>
</div>
<BarChart data={multiKillData} height={300} />
</Card>
<!-- Top Performers -->
<div class="grid gap-6 md:grid-cols-3">
{#if sortedPlayers.length > 0}
<!-- Most Kills -->
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<Trophy class="h-5 w-5 text-warning" />
<h3 class="font-semibold text-base-content">Most Kills</h3>
</div>
<div class="text-2xl font-bold text-base-content">{sortedPlayers[0].name}</div>
<div class="mt-1 text-3xl font-mono font-bold text-primary">{sortedPlayers[0].kills}</div>
<div class="mt-2 text-xs text-base-content/60">
{sortedPlayers[0].deaths} deaths, {sortedPlayers[0].kd.toFixed(2)} K/D
</div>
</Card>
<!-- Detailed Player Statistics Table -->
<Card padding="none">
<div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Detailed Player Statistics</h2>
<p class="mt-1 text-sm text-base-content/60">
Complete performance breakdown for all players
</p>
</div>
<!-- Best K/D -->
{@const bestKD = [...sortedPlayers].sort((a, b) => b.kd - a.kd)[0]}
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<Target class="h-5 w-5 text-success" />
<h3 class="font-semibold text-base-content">Best K/D Ratio</h3>
</div>
<div class="text-2xl font-bold text-base-content">{bestKD.name}</div>
<div class="mt-1 text-3xl font-mono font-bold text-success">{bestKD.kd.toFixed(2)}</div>
<div class="mt-2 text-xs text-base-content/60">
{bestKD.kills}K / {bestKD.deaths}D
</div>
</Card>
<DataTable data={sortedPlayers} columns={detailsColumns} striped hoverable />
</Card>
<!-- Most Utility Damage -->
{@const bestUtility = [...sortedPlayers].sort(
(a, b) => (b.ud_he || 0) + (b.ud_flames || 0) - ((a.ud_he || 0) + (a.ud_flames || 0))
)[0]}
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<Flame class="h-5 w-5 text-error" />
<h3 class="font-semibold text-base-content">Most Utility Damage</h3>
</div>
<div class="text-2xl font-bold text-base-content">{bestUtility.name}</div>
<div class="mt-1 text-3xl font-mono font-bold text-error">
{((bestUtility.ud_he || 0) + (bestUtility.ud_flames || 0)).toLocaleString()}
</div>
<div class="mt-2 text-xs text-base-content/60">
HE: {bestUtility.ud_he || 0} | Fire: {bestUtility.ud_flames || 0}
</div>
</Card>
{/if}
<!-- Top Performers -->
<div class="grid gap-6 md:grid-cols-3">
{#if sortedPlayers.length > 0}
<!-- Most Kills -->
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<Trophy class="h-5 w-5 text-warning" />
<h3 class="font-semibold text-base-content">Most Kills</h3>
</div>
<div class="text-2xl font-bold text-base-content">{sortedPlayers[0].name}</div>
<div class="mt-1 font-mono text-3xl font-bold text-primary">{sortedPlayers[0].kills}</div>
<div class="mt-2 text-xs text-base-content/60">
{sortedPlayers[0].deaths} deaths, {sortedPlayers[0].kd.toFixed(2)} K/D
</div>
</Card>
<!-- Best K/D -->
{@const bestKD = [...sortedPlayers].sort((a, b) => b.kd - a.kd)[0]}
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<Target class="h-5 w-5 text-success" />
<h3 class="font-semibold text-base-content">Best K/D Ratio</h3>
</div>
<div class="text-2xl font-bold text-base-content">{bestKD.name}</div>
<div class="mt-1 font-mono text-3xl font-bold text-success">{bestKD.kd.toFixed(2)}</div>
<div class="mt-2 text-xs text-base-content/60">
{bestKD.kills}K / {bestKD.deaths}D
</div>
</Card>
<!-- Most Utility Damage -->
{@const bestUtility = [...sortedPlayers].sort(
(a, b) => (b.ud_he || 0) + (b.ud_flames || 0) - ((a.ud_he || 0) + (a.ud_flames || 0))
)[0]}
<Card padding="lg">
<div class="mb-3 flex items-center gap-2">
<Flame class="h-5 w-5 text-error" />
<h3 class="font-semibold text-base-content">Most Utility Damage</h3>
</div>
<div class="text-2xl font-bold text-base-content">{bestUtility.name}</div>
<div class="mt-1 font-mono text-3xl font-bold text-error">
{((bestUtility.ud_he || 0) + (bestUtility.ud_flames || 0)).toLocaleString()}
</div>
<div class="mt-2 text-xs text-base-content/60">
HE: {bestUtility.ud_he || 0} | Fire: {bestUtility.ud_flames || 0}
</div>
</Card>
{/if}
</div>
</div>
</div>
{/if}