feat: Add player avatars to all match detail pages and player profile

- Add Steam avatars to match overview scoreboard (both teams)
- Add avatars to weapons page player table
- Add avatars to damage page player table and top dealer cards
- Add avatars to flashes page player table and Hall of Shame section
- Replace initial-based chat avatars with real Steam avatars
- Display actual Steam avatar on player profile page header

All avatar implementations include team-colored borders and fallback
to icons when avatar is unavailable.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-07 20:06:08 +01:00
parent 1024ba839e
commit 30b076bbec
6 changed files with 574 additions and 126 deletions

View File

@@ -1,14 +1,12 @@
<script lang="ts">
import { Trophy, Zap } from 'lucide-svelte';
import { Trophy, Zap, User } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte';
import PremierRatingBadge from '$lib/components/ui/PremierRatingBadge.svelte';
import RoundTimeline from '$lib/components/RoundTimeline.svelte';
import type { PageData } from './$types';
import type { MatchPlayer } from '$lib/types';
let { data }: { data: PageData } = $props();
const { match, rounds } = data;
const { match } = data;
// Group players by team - use dynamic team IDs from API
const uniqueTeamIds = match.players ? [...new Set(match.players.map((p) => p.team_id))] : [];
@@ -162,9 +160,22 @@
<td class="px-6 py-3">
<a
href={`/player/${player.id}`}
class="font-medium text-white transition-colors hover:text-neon-blue"
class="flex items-center gap-3 font-medium text-white transition-colors hover:text-neon-blue"
>
{player.name}
{#if player.avatar}
<img
src={player.avatar}
alt={player.name}
class="h-8 w-8 rounded-full border border-terrorist/30"
/>
{:else}
<div
class="flex h-8 w-8 items-center justify-center rounded-full bg-terrorist/20 text-terrorist"
>
<User class="h-4 w-4" />
</div>
{/if}
<span>{player.name}</span>
</a>
{#if player.id === mvpPlayerId}
<span
@@ -231,9 +242,22 @@
<td class="px-6 py-3">
<a
href={`/player/${player.id}`}
class="font-medium text-white transition-colors hover:text-neon-blue"
class="flex items-center gap-3 font-medium text-white transition-colors hover:text-neon-blue"
>
{player.name}
{#if player.avatar}
<img
src={player.avatar}
alt={player.name}
class="h-8 w-8 rounded-full border border-ct/30"
/>
{:else}
<div
class="flex h-8 w-8 items-center justify-center rounded-full bg-ct/20 text-ct"
>
<User class="h-4 w-4" />
</div>
{/if}
<span>{player.name}</span>
</a>
{#if player.id === mvpPlayerId}
<span
@@ -275,27 +299,4 @@
</div>
</div>
</Card>
<!-- Round Timeline -->
{#if rounds && rounds.rounds && rounds.rounds.length > 0}
<RoundTimeline rounds={rounds.rounds} maxRounds={match.max_rounds} />
{:else}
<Card padding="lg">
<div class="text-center">
<h3 class="mb-2 text-xl font-semibold text-white">Round Timeline</h3>
<p class="text-white/60">
{#if !match.demo_parsed}
Still processing the evidence of your crimes... Demo parsing in progress.
{:else}
Round-by-round timeline data is not available for this match.
{/if}
</p>
{#if !match.demo_parsed}
<Badge variant="warning" size="md" class="mt-4">Demo Not Yet Parsed</Badge>
{:else}
<Badge variant="info" size="md" class="mt-4">Round Data Not Available</Badge>
{/if}
</div>
</Card>
{/if}
</div>

View File

@@ -259,20 +259,33 @@
{@const player = match.players?.find((p) => p.id === String(message.player_id))}
{@const playerName =
message.player_name || player?.name || `Player ${message.player_id}`}
{@const playerAvatar = player?.avatar}
{@const teamId = player?.team_id || 0}
<div class="p-4 transition-colors hover:bg-white/5">
<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 {teamId ===
2
? 'bg-terrorist'
: teamId === 3
? 'bg-ct'
: 'bg-white/20'}"
>
{playerName.charAt(0).toUpperCase()}
</div>
{#if playerAvatar}
<img
src={playerAvatar}
alt={playerName}
class="h-10 w-10 rounded-full border {teamId === 2
? 'border-terrorist/50'
: teamId === 3
? 'border-ct/50'
: 'border-white/20'}"
/>
{:else}
<div
class="flex h-10 w-10 items-center justify-center rounded-full text-sm font-bold text-white {teamId ===
2
? 'bg-terrorist'
: teamId === 3
? 'bg-ct'
: 'bg-white/20'}"
>
{playerName.charAt(0).toUpperCase()}
</div>
{/if}
<!-- Message Content -->
<div class="min-w-0 flex-1">

View File

@@ -80,7 +80,12 @@
sortable: true,
render: (value: unknown, row: (typeof playersWithDamageStats)[0]) => {
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
return `<a href="/player/${row.id}" class="font-medium hover:text-neon-blue transition-colors ${teamClass}">${value}</a>`;
const borderClass = row.team_id === firstTeamId ? 'border-terrorist/30' : 'border-ct/30';
const bgClass = row.team_id === firstTeamId ? 'bg-terrorist/20' : 'bg-ct/20';
const avatarHtml = row.avatar
? `<img src="${row.avatar}" alt="${value}" class="h-8 w-8 rounded-full border ${borderClass}" />`
: `<div class="flex h-8 w-8 items-center justify-center rounded-full ${bgClass} ${teamClass}"><svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg></div>`;
return `<a href="/player/${row.id}" class="flex items-center gap-3 font-medium hover:text-neon-blue transition-colors ${teamClass}">${avatarHtml}<span>${value}</span></a>`;
}
},
{
@@ -243,11 +248,29 @@
</div>
<a
href={`/player/${player.id}`}
class="text-2xl font-bold transition-colors hover:text-neon-blue {player.team_id ===
class="flex items-center gap-3 text-2xl font-bold transition-colors hover:text-neon-blue {player.team_id ===
firstTeamId
? 'text-terrorist'
: 'text-ct'}"
>
{#if player.avatar}
<img
src={player.avatar}
alt={player.name}
class="h-10 w-10 rounded-full border {player.team_id === firstTeamId
? 'border-terrorist/30'
: 'border-ct/30'}"
/>
{:else}
<div
class="flex h-10 w-10 items-center justify-center rounded-full {player.team_id ===
firstTeamId
? 'bg-terrorist/20 text-terrorist'
: 'bg-ct/20 text-ct'}"
>
<Target class="h-5 w-5" />
</div>
{/if}
{player.name}
</a>
<div

View File

@@ -12,6 +12,7 @@
.map((player) => ({
name: player.name,
playerId: player.id,
avatar: player.avatar,
team_id: player.team_id,
enemies_blinded: player.flash_total_enemy || 0,
teammates_blinded: player.flash_total_team || 0,
@@ -59,6 +60,7 @@
interface FlashStat {
name: string;
playerId: string;
avatar: string;
team_id: number;
enemies_blinded: number;
teammates_blinded: number;
@@ -71,7 +73,21 @@
}
const columns = [
{ key: 'name' as const, label: 'Flashbang Criminal', sortable: true, width: '200px' },
{
key: 'name' as const,
label: 'Flashbang Criminal',
sortable: true,
width: '200px',
render: (value: unknown, row: FlashStat) => {
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
const borderClass = row.team_id === firstTeamId ? 'border-terrorist/30' : 'border-ct/30';
const bgClass = row.team_id === firstTeamId ? 'bg-terrorist/20' : 'bg-ct/20';
const avatarHtml = row.avatar
? `<img src="${row.avatar}" alt="${value}" class="h-8 w-8 rounded-full border ${borderClass}" />`
: `<div class="flex h-8 w-8 items-center justify-center rounded-full ${bgClass} ${teamClass}"><svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg></div>`;
return `<a href="/player/${row.playerId}" class="flex items-center gap-3 font-medium hover:text-neon-blue transition-colors ${teamClass}">${avatarHtml}<span>${value}</span></a>`;
}
},
{
key: 'enemies_blinded' as const,
label: 'Victims (Correct)',
@@ -197,8 +213,21 @@
</div>
<a
href={`/player/${shamePlayer.playerId}`}
class="font-medium text-white transition-colors hover:text-neon-blue"
class="flex items-center gap-3 font-medium text-white transition-colors hover:text-neon-blue"
>
{#if shamePlayer.avatar}
<img
src={shamePlayer.avatar}
alt={shamePlayer.name}
class="h-8 w-8 rounded-full border border-neon-red/30"
/>
{:else}
<div
class="flex h-8 w-8 items-center justify-center rounded-full bg-neon-red/20 text-neon-red"
>
<Eye class="h-4 w-4" />
</div>
{/if}
{shamePlayer.name}
</a>
</div>

View File

@@ -15,7 +15,7 @@
// Get player names map from match data
const playerNames = new Map(
match.players?.map((p) => [p.id, { name: p.name, team_id: p.team_id }]) || []
match.players?.map((p) => [p.id, { name: p.name, team_id: p.team_id, avatar: p.avatar }]) || []
);
// Get unique team IDs
@@ -40,6 +40,7 @@
return {
player_id: pw.player_id,
player_name: playerInfo?.name || 'Unknown',
player_avatar: playerInfo?.avatar || '',
team_id: playerInfo?.team_id || 2,
total_kills: totalKills,
total_damage: totalDamage,
@@ -64,7 +65,12 @@
render: (value: unknown, row: PlayerWeapon) => {
const strValue = value !== undefined ? String(value) : '';
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
return `<a href="/player/${row.player_id}" class="font-medium hover:text-neon-blue transition-colors ${teamClass}">${strValue}</a>`;
const borderClass = row.team_id === firstTeamId ? 'border-terrorist/30' : 'border-ct/30';
const bgClass = row.team_id === firstTeamId ? 'bg-terrorist/20' : 'bg-ct/20';
const avatarHtml = row.player_avatar
? `<img src="${row.player_avatar}" alt="${strValue}" class="h-8 w-8 rounded-full border ${borderClass}" />`
: `<div class="flex h-8 w-8 items-center justify-center rounded-full ${bgClass} ${teamClass}"><svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg></div>`;
return `<a href="/player/${row.player_id}" class="flex items-center gap-3 font-medium hover:text-neon-blue transition-colors ${teamClass}">${avatarHtml}<span>${strValue}</span></a>`;
}
},
{

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import {
User,
CircleUser,
Users,
Target,
TrendingUp,
@@ -22,7 +22,14 @@
Medal,
ArrowUpRight,
ArrowDownRight,
Minus
Minus,
Gauge,
Clock,
Wifi,
Shield,
Sparkles,
Sun,
Moon
} from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte';
import Button from '$lib/components/ui/Button.svelte';
@@ -38,7 +45,7 @@
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
const { profile, recentMatches, playerStats, metaStats } = data;
const { profile, recentMatches, playerStats, metaStats, calculatedStats } = data;
// Track this player visit
onMount(() => {
@@ -329,10 +336,14 @@
<div class="flex flex-col items-start gap-6 md:flex-row md:items-center">
<!-- Avatar with Neon Ring -->
<div
class="flex h-24 w-24 items-center justify-center rounded-full border-2 border-neon-blue bg-void"
class="flex h-24 w-24 items-center justify-center overflow-hidden rounded-full border-2 border-neon-blue bg-void"
style="box-shadow: 0 0 30px rgba(0, 212, 255, 0.3);"
>
<User class="h-12 w-12 text-neon-blue" />
{#if profile.avatar}
<img src={profile.avatar} alt={profile.name} class="h-full w-full object-cover" />
{:else}
<CircleUser class="h-14 w-14 text-neon-blue/70" />
{/if}
</div>
<!-- Info -->
@@ -807,22 +818,302 @@
</div>
{/if}
<!-- Teammates Section (from pre-aggregated meta stats) -->
{#if metaStats && (metaStats.best_mates?.length || metaStats.most_mates?.length)}
<div>
<div class="mb-4 flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-blue/20"
style="box-shadow: 0 0 15px rgba(0, 212, 255, 0.2);"
>
<Users class="h-5 w-5 text-neon-blue" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Teammates</h2>
<p class="text-sm text-white/50">Who they play with</p>
</div>
<!-- Playstyle Analysis Section -->
<div>
<div class="mb-4 flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-green/20"
style="box-shadow: 0 0 15px rgba(0, 255, 136, 0.2);"
>
<Shield class="h-5 w-5 text-neon-green" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Playstyle Analysis</h2>
<p class="text-sm text-white/50">How they play the game</p>
</div>
</div>
{#if calculatedStats}
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<!-- Detected Role -->
<Card padding="lg">
<div class="mb-2 flex items-center gap-2">
<Target class="h-5 w-5 text-neon-blue" />
<span class="text-sm font-medium text-white/70">Detected Role</span>
</div>
<div
class="font-mono text-2xl font-bold text-neon-blue"
style="text-shadow: 0 0 15px rgba(0, 212, 255, 0.4);"
>
{calculatedStats.detectedRole}
</div>
<div class="mt-1 text-xs text-white/50">
{calculatedStats.roleConfidence}% confidence
</div>
</Card>
<!-- Form Indicator -->
<Card
padding="lg"
class={calculatedStats.formRating === 'hot'
? 'border-neon-green/30'
: calculatedStats.formRating === 'cold'
? 'border-neon-red/30'
: ''}
>
<div class="mb-2 flex items-center gap-2">
{#if calculatedStats.formRating === 'hot'}
<Flame class="h-5 w-5 text-neon-green" />
{:else if calculatedStats.formRating === 'cold'}
<Moon class="h-5 w-5 text-neon-blue" />
{:else}
<Gauge class="h-5 w-5 text-neon-purple" />
{/if}
<span class="text-sm font-medium text-white/70">Current Form</span>
</div>
<div
class="font-mono text-2xl font-bold {calculatedStats.formRating === 'hot'
? 'text-neon-green'
: calculatedStats.formRating === 'cold'
? 'text-neon-blue'
: 'text-neon-purple'}"
style="text-shadow: 0 0 15px {calculatedStats.formRating === 'hot'
? 'rgba(0, 255, 136, 0.4)'
: calculatedStats.formRating === 'cold'
? 'rgba(0, 212, 255, 0.4)'
: 'rgba(139, 92, 246, 0.4)'};"
>
{calculatedStats.formRating === 'hot'
? 'On Fire'
: calculatedStats.formRating === 'cold'
? 'Cold Streak'
: 'Steady'}
</div>
<div class="mt-1 text-xs text-white/50">
{calculatedStats.formDelta > 0 ? '+' : ''}{calculatedStats.formDelta.toFixed(1)}% vs
avg
</div>
</Card>
<!-- Side Preference -->
<Card padding="lg">
<div class="mb-2 flex items-center gap-2">
<Users class="h-5 w-5 text-neon-gold" />
<span class="text-sm font-medium text-white/70">Side Preference</span>
</div>
<div
class="font-mono text-2xl font-bold {calculatedStats.preferredSide === 'CT'
? 'text-ct'
: calculatedStats.preferredSide === 'T'
? 'text-terrorist'
: 'text-white'}"
>
{calculatedStats.preferredSide === 'CT'
? 'CT-Sided'
: calculatedStats.preferredSide === 'T'
? 'T-Sided'
: 'Balanced'}
</div>
<div class="mt-1 flex gap-2 text-xs">
<span class="text-ct">CT: {calculatedStats.ctSideWinRate.toFixed(0)}%</span>
<span class="text-white/30">|</span>
<span class="text-terrorist">T: {calculatedStats.tSideWinRate.toFixed(0)}%</span>
</div>
</Card>
<!-- Consistency -->
<Card padding="lg">
<div class="mb-2 flex items-center gap-2">
<Activity class="h-5 w-5 text-neon-purple" />
<span class="text-sm font-medium text-white/70">Consistency</span>
</div>
<div
class="font-mono text-2xl font-bold {calculatedStats.consistencyRating ===
'very_consistent' || calculatedStats.consistencyRating === 'consistent'
? 'text-neon-green'
: 'text-neon-red'}"
style="text-shadow: 0 0 15px {calculatedStats.consistencyRating ===
'very_consistent' || calculatedStats.consistencyRating === 'consistent'
? 'rgba(0, 255, 136, 0.4)'
: 'rgba(255, 51, 102, 0.4)'};"
>
{calculatedStats.consistencyRating === 'very_consistent'
? 'Rock Solid'
: calculatedStats.consistencyRating === 'consistent'
? 'Reliable'
: calculatedStats.consistencyRating === 'inconsistent'
? 'Variable'
: 'Unpredictable'}
</div>
<div class="mt-1 text-xs text-white/50">
{calculatedStats.kdConsistency.toFixed(1)}% K/D variance
</div>
</Card>
</div>
{:else}
<Card padding="lg" class="border-neon-green/10 bg-neon-green/5">
<div class="flex items-center gap-4">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-green/10">
<Shield class="h-6 w-6 text-neon-green/50" />
</div>
<div>
<h3 class="text-lg font-semibold text-white">Not Enough Match Data</h3>
<p class="text-sm text-white/50">
Playstyle analysis requires detailed match data. Play more matches to unlock role
detection, form tracking, and consistency analysis.
</p>
</div>
</div>
</Card>
{/if}
</div>
<!-- Streaks & Trends Section -->
<div>
<div class="mb-4 flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-gold/20"
style="box-shadow: 0 0 15px rgba(255, 215, 0, 0.2);"
>
<Sparkles class="h-5 w-5 text-neon-gold" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Streaks & Trends</h2>
<p class="text-sm text-white/50">Win patterns and timing</p>
</div>
</div>
{#if calculatedStats}
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<!-- Current Streak -->
<Card
padding="lg"
class={calculatedStats.currentStreak.type === 'W'
? 'border-neon-green/30'
: calculatedStats.currentStreak.type === 'L'
? 'border-neon-red/30'
: ''}
>
<div class="mb-2 flex items-center gap-2">
{#if calculatedStats.currentStreak.type === 'W'}
<TrendingUp class="h-5 w-5 text-neon-green" />
{:else if calculatedStats.currentStreak.type === 'L'}
<ArrowDownRight class="h-5 w-5 text-neon-red" />
{:else}
<Minus class="h-5 w-5 text-neon-blue" />
{/if}
<span class="text-sm font-medium text-white/70">Current Streak</span>
</div>
<div
class="font-mono text-3xl font-bold {calculatedStats.currentStreak.type === 'W'
? 'text-neon-green'
: calculatedStats.currentStreak.type === 'L'
? 'text-neon-red'
: 'text-neon-blue'}"
style="text-shadow: 0 0 15px {calculatedStats.currentStreak.type === 'W'
? 'rgba(0, 255, 136, 0.4)'
: calculatedStats.currentStreak.type === 'L'
? 'rgba(255, 51, 102, 0.4)'
: 'rgba(0, 212, 255, 0.4)'};"
>
{calculatedStats.currentStreak.count}{calculatedStats.currentStreak.type}
</div>
<div class="mt-1 text-xs text-white/50">
{calculatedStats.currentStreak.type === 'W'
? 'Keep it going!'
: calculatedStats.currentStreak.type === 'L'
? 'Time to bounce back'
: 'Draw streak'}
</div>
</Card>
<!-- Best Win Streak -->
<Card padding="lg">
<div class="mb-2 flex items-center gap-2">
<Crown class="h-5 w-5 text-neon-gold" />
<span class="text-sm font-medium text-white/70">Best Win Streak</span>
</div>
<div
class="font-mono text-3xl font-bold text-neon-gold"
style="text-shadow: 0 0 15px rgba(255, 215, 0, 0.4);"
>
{calculatedStats.longestWinStreak}W
</div>
<div class="mt-1 text-xs text-white/50">Personal best</div>
</Card>
<!-- Weekend vs Weekday -->
<Card padding="lg">
<div class="mb-2 flex items-center gap-2">
<Sun class="h-5 w-5 text-neon-gold" />
<span class="text-sm font-medium text-white/70">Weekend Performance</span>
</div>
<div
class="font-mono text-3xl font-bold {calculatedStats.weekendWinRate >
calculatedStats.weekdayWinRate
? 'text-neon-green'
: 'text-neon-blue'}"
style="text-shadow: 0 0 15px {calculatedStats.weekendWinRate >
calculatedStats.weekdayWinRate
? 'rgba(0, 255, 136, 0.4)'
: 'rgba(0, 212, 255, 0.4)'};"
>
{calculatedStats.weekendWinRate.toFixed(0)}%
</div>
<div class="mt-1 text-xs text-white/50">
Weekday: {calculatedStats.weekdayWinRate.toFixed(0)}%
</div>
</Card>
<!-- Best Time of Day -->
<Card padding="lg">
<div class="mb-2 flex items-center gap-2">
<Clock class="h-5 w-5 text-neon-purple" />
<span class="text-sm font-medium text-white/70">Peak Time</span>
</div>
<div
class="font-mono text-2xl font-bold text-neon-purple"
style="text-shadow: 0 0 15px rgba(139, 92, 246, 0.4);"
>
{calculatedStats.bestTimeOfDay}
</div>
<div class="mt-1 text-xs text-white/50">Best win rate time slot</div>
</Card>
</div>
{:else}
<Card padding="lg" class="border-neon-gold/10 bg-neon-gold/5">
<div class="flex items-center gap-4">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-gold/10">
<Sparkles class="h-6 w-6 text-neon-gold/50" />
</div>
<div>
<h3 class="text-lg font-semibold text-white">Not Enough Match Data</h3>
<p class="text-sm text-white/50">
Streak and trend analysis requires match history with results. Play more matches to
see win streaks, time-of-day performance, and weekend vs weekday stats.
</p>
</div>
</div>
</Card>
{/if}
</div>
<!-- Teammates Section (from pre-aggregated meta stats) -->
<div>
<div class="mb-4 flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-blue/20"
style="box-shadow: 0 0 15px rgba(0, 212, 255, 0.2);"
>
<Users class="h-5 w-5 text-neon-blue" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Teammates</h2>
<p class="text-sm text-white/50">Who they play with</p>
</div>
</div>
{#if metaStats && (metaStats.best_mates?.length || metaStats.most_mates?.length)}
<div class="grid gap-6 lg:grid-cols-2">
<!-- Best Teammates (by win rate) -->
{#if metaStats.best_mates && metaStats.best_mates.length > 0}
@@ -839,16 +1130,23 @@
class="group flex items-center gap-3 rounded-lg bg-white/5 p-3 transition-all hover:bg-white/10"
>
<div
class="flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-void"
class="relative flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-white/10 bg-void"
>
{#if teammate.player.avatar}
<img
src={teammate.player.avatar}
alt={teammate.player.name || 'Player'}
class="h-full w-full rounded-full"
class="h-full w-full rounded-full object-cover"
onerror={(e) => {
const target = e.currentTarget as HTMLImageElement;
target.style.display = 'none';
const fallback = target.nextElementSibling as HTMLElement;
if (fallback) fallback.style.display = 'block';
}}
/>
<CircleUser class="hidden h-6 w-6 text-white/40" />
{:else}
<User class="h-5 w-5 text-white/50" />
<CircleUser class="h-6 w-6 text-white/40" />
{/if}
</div>
<div class="min-w-0 flex-1">
@@ -897,16 +1195,23 @@
class="group flex items-center gap-3 rounded-lg bg-white/5 p-3 transition-all hover:bg-white/10"
>
<div
class="flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-void"
class="relative flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-white/10 bg-void"
>
{#if teammate.player.avatar}
<img
src={teammate.player.avatar}
alt={teammate.player.name || 'Player'}
class="h-full w-full rounded-full"
class="h-full w-full rounded-full object-cover"
onerror={(e) => {
const target = e.currentTarget as HTMLImageElement;
target.style.display = 'none';
const fallback = target.nextElementSibling as HTMLElement;
if (fallback) fallback.style.display = 'block';
}}
/>
<CircleUser class="hidden h-6 w-6 text-white/40" />
{:else}
<User class="h-5 w-5 text-white/50" />
<CircleUser class="h-6 w-6 text-white/40" />
{/if}
</div>
<div class="min-w-0 flex-1">
@@ -942,27 +1247,42 @@
</Card>
{/if}
</div>
</div>
{/if}
{:else}
<Card padding="lg" class="border-neon-blue/10 bg-neon-blue/5">
<div class="flex items-center gap-4">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-blue/10">
<Users class="h-6 w-6 text-neon-blue/50" />
</div>
<div>
<h3 class="text-lg font-semibold text-white">Teammate Data Unavailable</h3>
<p class="text-sm text-white/50">
Teammate history is calculated from match data. This player needs more matches or
the data is still being processed.
</p>
</div>
</div>
</Card>
{/if}
</div>
<!-- Weapon Preferences Section (from pre-aggregated meta stats) -->
{#if metaStats && metaStats.weapon_dmg && metaStats.weapon_dmg.length > 0}
{@const topWeapons = metaStats.weapon_dmg.slice(0, 8)}
{@const maxDamage = Math.max(...topWeapons.map((w) => w.dmg))}
<div>
<div class="mb-4 flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-gold/20"
style="box-shadow: 0 0 15px rgba(255, 215, 0, 0.2);"
>
<Crosshair class="h-5 w-5 text-neon-gold" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Weapon Preferences</h2>
<p class="text-sm text-white/50">Damage dealt by weapon type</p>
</div>
<div>
<div class="mb-4 flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-gold/20"
style="box-shadow: 0 0 15px rgba(255, 215, 0, 0.2);"
>
<Crosshair class="h-5 w-5 text-neon-gold" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Weapon Preferences</h2>
<p class="text-sm text-white/50">Damage dealt by weapon type</p>
</div>
</div>
{#if metaStats && metaStats.weapon_dmg && metaStats.weapon_dmg.length > 0}
{@const topWeapons = metaStats.weapon_dmg.slice(0, 8)}
{@const maxDamage = Math.max(...topWeapons.map((w) => w.dmg))}
<Card padding="lg">
<div class="space-y-4">
{#each topWeapons as weapon, index}
@@ -1001,44 +1321,23 @@
{/each}
</div>
</Card>
</div>
{/if}
<!-- Empty State for Unparsed Matches -->
{#if playerStats.length === 0 && recentMatches.length > 0}
<Card padding="lg">
<div class="flex items-center gap-4">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-white/5">
<Swords class="h-6 w-6 text-white/30" />
{:else}
<Card padding="lg" class="border-neon-gold/10 bg-neon-gold/5">
<div class="flex items-center gap-4">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-gold/10">
<Crosshair class="h-6 w-6 text-neon-gold/50" />
</div>
<div>
<h3 class="text-lg font-semibold text-white">Weapon Data Unavailable</h3>
<p class="text-sm text-white/50">
Weapon statistics are calculated from match data. This player needs more matches or
the data is still being processed.
</p>
</div>
</div>
<div>
<h3 class="text-lg font-semibold text-white">Detailed Stats Unavailable</h3>
<p class="text-sm text-white/50">
This player's matches haven't been parsed yet. Detailed combat stats, multi-kills,
rating progression, and utility analysis require parsed demo files.
</p>
</div>
</div>
</Card>
{/if}
<!-- Info when meta stats unavailable (teammates, weapons) -->
{#if !metaStats}
<Card padding="lg">
<div class="flex items-center gap-4">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-blue/10">
<Users class="h-6 w-6 text-neon-blue/50" />
</div>
<div>
<h3 class="text-lg font-semibold text-white">Teammate & Weapon Stats Loading</h3>
<p class="text-sm text-white/50">
Teammate history and weapon preferences are being calculated. This data is cached and
may take a moment to generate on first visit.
</p>
</div>
</div>
</Card>
{/if}
</Card>
{/if}
</div>
<!-- Recent Matches -->
<div>
@@ -1286,6 +1585,83 @@
plays
</div>
</Card>
<!-- Team Damage -->
{#if calculatedStats}
<Card padding="lg" class="border-neon-red/20">
<div class="mb-2 flex items-center gap-2">
<AlertTriangle class="h-5 w-5 text-neon-red" />
<span class="text-sm font-medium text-white/70">Team Damage</span>
</div>
<div
class="font-mono text-3xl font-bold text-neon-red"
style="text-shadow: 0 0 15px rgba(255, 51, 102, 0.4);"
>
{Math.round(calculatedStats.avgTeamDamage)}
</div>
<div class="mt-1 text-xs text-white/50">
{calculatedStats.teamDamageRatio.toFixed(1)}% of total damage - Oops
</div>
</Card>
<!-- Avg Ping -->
{#if calculatedStats.avgPing > 0}
<Card padding="lg">
<div class="mb-2 flex items-center gap-2">
<Wifi class="h-5 w-5 text-neon-blue" />
<span class="text-sm font-medium text-white/70">Avg Ping</span>
</div>
<div
class="font-mono text-3xl font-bold {calculatedStats.avgPing < 50
? 'text-neon-green'
: calculatedStats.avgPing < 100
? 'text-neon-gold'
: 'text-neon-red'}"
style="text-shadow: 0 0 15px {calculatedStats.avgPing < 50
? 'rgba(0, 255, 136, 0.4)'
: calculatedStats.avgPing < 100
? 'rgba(255, 215, 0, 0.4)'
: 'rgba(255, 51, 102, 0.4)'};"
>
{Math.round(calculatedStats.avgPing)}ms
</div>
<div class="mt-1 text-xs text-white/50">
{calculatedStats.avgPing < 50
? 'Excellent connection'
: calculatedStats.avgPing < 100
? 'Good connection'
: 'High latency'}
</div>
</Card>
{:else}
<Card padding="lg" class="border-white/5 bg-white/5">
<div class="mb-2 flex items-center gap-2">
<Wifi class="h-5 w-5 text-white/30" />
<span class="text-sm font-medium text-white/70">Avg Ping</span>
</div>
<div class="font-mono text-2xl font-bold text-white/30">N/A</div>
<div class="mt-1 text-xs text-white/40">Ping data not available</div>
</Card>
{/if}
{:else}
<Card padding="lg" class="border-white/5 bg-white/5">
<div class="mb-2 flex items-center gap-2">
<AlertTriangle class="h-5 w-5 text-white/30" />
<span class="text-sm font-medium text-white/70">Team Damage</span>
</div>
<div class="font-mono text-2xl font-bold text-white/30">N/A</div>
<div class="mt-1 text-xs text-white/40">Requires match data</div>
</Card>
<Card padding="lg" class="border-white/5 bg-white/5">
<div class="mb-2 flex items-center gap-2">
<Wifi class="h-5 w-5 text-white/30" />
<span class="text-sm font-medium text-white/70">Avg Ping</span>
</div>
<div class="font-mono text-2xl font-bold text-white/30">N/A</div>
<div class="mt-1 text-xs text-white/40">Requires match data</div>
</Card>
{/if}
</div>
</div>
{/if}