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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user