feat: Add player profile performance charts and visualizations

Implemented comprehensive performance analysis for player profiles with interactive charts
and detailed statistics visualization.

Key Features:
- Performance Trend Chart (K/D and KAST over last 15 matches)
- Map Performance Chart (win rate per map with color coding)
- Utility Effectiveness Stats (flash assists, enemies blinded, HE/flame damage)
- Responsive charts using Chart.js LineChart and BarChart components

Technical Updates:
- Enhanced page loader to fetch 15 detailed matches with player stats
- Fixed DataTable Svelte 5 compatibility and type safety
- Updated MatchCard and PlayerCard to use PlayerMeta properties
- Proper error handling and typed data structures

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-04 21:49:36 +01:00
parent 274f5b3b53
commit 8b73a68a6b
6 changed files with 328 additions and 96 deletions

View File

@@ -1,12 +1,13 @@
<script lang="ts" generics="T">
/* eslint-disable no-undef */
import { ArrowUp, ArrowDown } from 'lucide-svelte';
interface Column<T> {
key: keyof T;
label: string;
sortable?: boolean;
format?: (value: any, row: T) => string;
render?: (value: any, row: T) => any;
format?: (value: T[keyof T], row: T) => string;
render?: (value: T[keyof T], row: T) => unknown;
align?: 'left' | 'center' | 'right';
class?: string;
}
@@ -43,19 +44,19 @@
}
};
const sortedData = $derived(() => {
if (!sortKey) return data;
const sortedData = $derived(
!sortKey
? data
: [...data].sort((a, b) => {
const aVal = a[sortKey as keyof T];
const bVal = b[sortKey as keyof T];
return [...data].sort((a, b) => {
const aVal = a[sortKey];
const bVal = b[sortKey];
if (aVal === bVal) return 0;
if (aVal === bVal) return 0;
const comparison = aVal < bVal ? -1 : 1;
return sortDirection === 'asc' ? comparison : -comparison;
});
})();
const comparison = aVal < bVal ? -1 : 1;
return sortDirection === 'asc' ? comparison : -comparison;
})
);
const getValue = (row: T, column: Column<T>) => {
const value = row[column.key];
@@ -77,7 +78,11 @@
class="text-{column.align || 'left'} {column.class || ''}"
onclick={() => handleSort(column)}
>
<div class="flex items-center gap-2" class:justify-end={column.align === 'right'} class:justify-center={column.align === 'center'}>
<div
class="flex items-center gap-2"
class:justify-end={column.align === 'right'}
class:justify-center={column.align === 'center'}
>
<span>{column.label}</span>
{#if column.sortable}
<div class="flex flex-col opacity-40">
@@ -87,7 +92,7 @@
: ''}"
/>
<ArrowDown
class="h-3 w-3 -mt-1 {sortKey === column.key && sortDirection === 'desc'
class="-mt-1 h-3 w-3 {sortKey === column.key && sortDirection === 'desc'
? 'text-primary opacity-100'
: ''}"
/>
@@ -99,7 +104,7 @@
</tr>
</thead>
<tbody>
{#each sortedData as row, i}
{#each sortedData as row}
<tr class:hover={hoverable}>
{#each columns as column}
<td class="text-{column.align || 'left'} {column.class || ''}">

View File

@@ -54,13 +54,13 @@
{/if}
</div>
<!-- Result Badge -->
<!-- Result Badge (inferred from score) -->
<div class="mt-3 flex justify-center">
{#if match.match_result === 0}
{#if match.score_team_a === match.score_team_b}
<Badge variant="warning" size="sm">Tie</Badge>
{:else if match.match_result === 1}
{:else if match.score_team_a > match.score_team_b}
<Badge variant="success" size="sm">Team A Win</Badge>
{:else if match.match_result === 2}
{:else}
<Badge variant="error" size="sm">Team B Win</Badge>
{/if}
</div>

View File

@@ -10,10 +10,11 @@
let { player, showStats = true }: Props = $props();
const kd = player.deaths > 0 ? (player.kills / player.deaths).toFixed(2) : player.kills.toFixed(2);
const winRate = player.wins + player.losses > 0
? ((player.wins / (player.wins + player.losses)) * 100).toFixed(1)
: '0.0';
const kd =
player.avg_deaths > 0
? (player.avg_kills / player.avg_deaths).toFixed(2)
: player.avg_kills.toFixed(2);
const winRate = (player.win_rate * 100).toFixed(1);
</script>
<a
@@ -26,7 +27,7 @@
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-base-100">
<User class="h-6 w-6 text-primary" />
</div>
<div class="flex-1 min-w-0">
<div class="min-w-0 flex-1">
<h3 class="truncate text-lg font-bold text-base-content">{player.name}</h3>
<p class="text-sm text-base-content/60">ID: {player.id}</p>
</div>
@@ -56,7 +57,7 @@
<div class="mb-1 flex items-center justify-center">
<User class="mr-1 h-4 w-4 text-info" />
</div>
<div class="text-xl font-bold text-base-content">{player.wins + player.losses}</div>
<div class="text-xl font-bold text-base-content">{player.recent_matches}</div>
<div class="text-xs text-base-content/60">Matches</div>
</div>
</div>
@@ -64,11 +65,8 @@
<!-- Footer -->
<div class="border-t border-base-300 bg-base-200 px-4 py-3">
<div class="flex items-center justify-between text-sm">
<span class="text-base-content/60">Record:</span>
<div class="flex gap-2">
<Badge variant="success" size="sm">{player.wins}W</Badge>
<Badge variant="error" size="sm">{player.losses}L</Badge>
</div>
<span class="text-base-content/60">Avg KAST:</span>
<Badge variant="info" size="sm">{player.avg_kast.toFixed(1)}%</Badge>
</div>
</div>
{/if}