forked from CSGOWTF/csgowtf
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:
@@ -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 || ''}">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user