style: Redesign match detail pages with neon esports aesthetic

Complete overhaul of all 7 match sub-pages (Overview, Flashes, Economy,
Details, Weapons, Damage, Chat) with consistent neon design system.

Key changes:
- Update Card/Tabs components with void backgrounds and neon accents
- Add decorative blur orbs and grid pattern to match layout hero
- Convert DaisyUI classes to custom Tailwind with neon colors
- Update chart components with neon-themed tooltips and grid styling
- Add RoundTimeline neon glow on selection with void-themed tooltips

Puns added throughout:
- "Hall of Shame" for players who flash teammates more than enemies
- "Needs Therapy Award" for high team damage
- "MVP (Most Violent Player)" badge
- "The Poverty Round", "YOLO Buy" economy labels
- "Multi-Threat Level", "Can't Touch This", "Molotov Mixologist"

🤖 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 17:54:29 +01:00
parent 51112df979
commit ee233bb6fb
14 changed files with 1174 additions and 565 deletions

View File

@@ -72,9 +72,9 @@
<Card padding="lg"> <Card padding="lg">
<div class="mb-6"> <div class="mb-6">
<h2 class="text-2xl font-bold text-base-content">Round Timeline</h2> <h2 class="text-2xl font-bold text-white">Round Timeline</h2>
<p class="mt-2 text-sm text-base-content/60"> <p class="mt-2 text-sm text-white/60">
Click on a round to see detailed information. T = Terrorists, CT = Counter-Terrorists Click on a round to see the battle details. T = Terrorists, CT = Counter-Terrorists
</p> </p>
</div> </div>
@@ -100,25 +100,20 @@
> >
<!-- Round number --> <!-- Round number -->
<div <div
class="mb-2 text-xs font-semibold transition-colors" class="mb-2 text-xs font-semibold transition-colors {isSelected
class:text-primary={isSelected} ? 'text-neon-blue'
class:opacity-60={!isSelected} : 'text-white/60'}"
> >
{round.round} {round.round}
</div> </div>
<!-- Round indicator circle --> <!-- Round indicator circle -->
<div <div
class="relative flex h-12 w-12 items-center justify-center rounded-full border-2 transition-all" class="relative flex h-12 w-12 items-center justify-center rounded-full border-2 transition-all {isWinner2
class:border-terrorist={isWinner2} ? 'border-terrorist bg-terrorist/20'
class:bg-terrorist={isWinner2} : ''} {isWinner3 ? 'border-ct bg-ct/20' : ''} {isSelected
class:bg-opacity-20={isWinner2 || isWinner3} ? 'scale-110 shadow-[0_0_15px_rgba(0,212,255,0.4)] ring-2 ring-neon-blue'
class:border-ct={isWinner3} : ''}"
class:bg-ct={isWinner3}
class:ring-4={isSelected}
class:ring-primary={isSelected}
class:ring-opacity-30={isSelected}
class:scale-110={isSelected}
> >
<!-- Win reason icon or T/CT badge --> <!-- Win reason icon or T/CT badge -->
{#if Icon} {#if Icon}
@@ -147,18 +142,18 @@
<!-- Connecting line to next round --> <!-- Connecting line to next round -->
{#if round.round < rounds.length} {#if round.round < rounds.length}
<div <div
class="absolute left-[60px] top-[34px] h-0.5 w-[calc(100%-60px)] bg-base-300" class="absolute left-[60px] top-[34px] h-0.5 w-[calc(100%-60px)] bg-white/10"
></div> ></div>
{/if} {/if}
<!-- Hover tooltip --> <!-- Hover tooltip -->
<div <div
class="pointer-events-none absolute top-full z-10 mt-2 hidden w-48 rounded-lg bg-base-100 p-3 text-left shadow-xl ring-1 ring-base-300 group-hover:block" class="pointer-events-none absolute top-full z-10 mt-2 hidden w-48 rounded-lg border border-white/10 bg-void-light p-3 text-left shadow-xl backdrop-blur-sm group-hover:block"
> >
<div class="text-xs font-semibold text-base-content"> <div class="text-xs font-semibold text-white">
Round {round.round} Round {round.round}
</div> </div>
<div class="mt-1 text-xs text-base-content/80"> <div class="mt-1 text-xs text-white/80">
Winner: Winner:
<span <span
class="font-bold" class="font-bold"
@@ -168,10 +163,10 @@
{isWinner2 ? 'Terrorists' : 'Counter-Terrorists'} {isWinner2 ? 'Terrorists' : 'Counter-Terrorists'}
</span> </span>
</div> </div>
<div class="mt-1 text-xs text-base-content/60"> <div class="mt-1 text-xs text-white/60">
{getWinReasonText(round.win_reason)} {getWinReasonText(round.win_reason)}
</div> </div>
<div class="mt-2 text-xs text-base-content/60"> <div class="mt-2 text-xs text-white/60">
Score: {scoreAtRound.teamA} - {scoreAtRound.teamB} Score: {scoreAtRound.teamA} - {scoreAtRound.teamB}
</div> </div>
</div> </div>
@@ -196,13 +191,13 @@
<!-- Selected Round Details --> <!-- Selected Round Details -->
{#if selectedRoundData} {#if selectedRoundData}
<div class="mt-6 border-t border-base-300 pt-6"> <div class="mt-6 border-t border-white/10 pt-6">
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h3 class="text-xl font-bold text-base-content"> <h3 class="text-xl font-bold text-white">
Round {selectedRoundData.round} Details Round {selectedRoundData.round} Details
</h3> </h3>
<button <button
class="btn btn-ghost btn-sm" class="rounded-lg px-3 py-1.5 text-sm text-white/60 transition-colors hover:bg-white/5 hover:text-white"
onclick={() => (selectedRound = null)} onclick={() => (selectedRound = null)}
aria-label="Close details" aria-label="Close details"
> >
@@ -212,7 +207,7 @@
<div class="grid gap-4 md:grid-cols-2"> <div class="grid gap-4 md:grid-cols-2">
<div> <div>
<div class="text-sm text-base-content/60">Winner</div> <div class="text-sm text-white/50">Winner</div>
<div <div
class="text-lg font-bold" class="text-lg font-bold"
class:text-terrorist={selectedRoundData.winner === 2} class:text-terrorist={selectedRoundData.winner === 2}
@@ -222,8 +217,8 @@
</div> </div>
</div> </div>
<div> <div>
<div class="text-sm text-base-content/60">Win Reason</div> <div class="text-sm text-white/50">Win Reason</div>
<div class="text-lg font-semibold text-base-content"> <div class="text-lg font-semibold text-white">
{getWinReasonText(selectedRoundData.win_reason)} {getWinReasonText(selectedRoundData.win_reason)}
</div> </div>
</div> </div>
@@ -232,37 +227,46 @@
<!-- Player stats for the round if available --> <!-- Player stats for the round if available -->
{#if selectedRoundData.players && selectedRoundData.players.length > 0} {#if selectedRoundData.players && selectedRoundData.players.length > 0}
<div class="mt-4"> <div class="mt-4">
<h4 class="mb-2 text-sm font-semibold text-base-content">Round Economy</h4> <h4 class="mb-2 text-sm font-semibold text-white">Round Economy</h4>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-sm"> <table class="w-full text-sm">
<thead> <thead>
<tr class="border-base-300"> <tr class="border-b border-white/10 text-left text-white/50">
<th>Player</th> <th class="px-3 py-2">Player</th>
<th>Bank</th> <th class="px-3 py-2">Bank</th>
<th>Equipment</th> <th class="px-3 py-2">Equipment</th>
<th>Spent</th> <th class="px-3 py-2">Spent</th>
{#if selectedRoundData.players.some((p) => p.kills_in_round !== undefined)} {#if selectedRoundData.players.some((p) => p.kills_in_round !== undefined)}
<th>Kills</th> <th class="px-3 py-2">Kills</th>
{/if} {/if}
{#if selectedRoundData.players.some((p) => p.damage_in_round !== undefined)} {#if selectedRoundData.players.some((p) => p.damage_in_round !== undefined)}
<th>Damage</th> <th class="px-3 py-2">Damage</th>
{/if} {/if}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each selectedRoundData.players as player} {#each selectedRoundData.players as player}
<tr class="border-base-300"> <tr class="border-b border-white/5 transition-colors hover:bg-white/5">
<td class="font-medium" <td class="px-3 py-2 font-medium text-white"
>Player {player.player_id || player.match_player_id || '?'}</td >Player {player.player_id || player.match_player_id || '?'}</td
> >
<td class="font-mono text-success">${player.bank.toLocaleString()}</td> <td class="px-3 py-2 font-mono text-neon-green"
<td class="font-mono">${player.equipment.toLocaleString()}</td> >${player.bank.toLocaleString()}</td
<td class="font-mono text-error">${player.spent.toLocaleString()}</td> >
<td class="px-3 py-2 font-mono text-white/80"
>${player.equipment.toLocaleString()}</td
>
<td class="px-3 py-2 font-mono text-neon-red"
>${player.spent.toLocaleString()}</td
>
{#if selectedRoundData.players.some((p) => p.kills_in_round !== undefined)} {#if selectedRoundData.players.some((p) => p.kills_in_round !== undefined)}
<td class="font-mono">{player.kills_in_round || 0}</td> <td class="px-3 py-2 font-mono text-white/80">{player.kills_in_round || 0}</td
>
{/if} {/if}
{#if selectedRoundData.players.some((p) => p.damage_in_round !== undefined)} {#if selectedRoundData.players.some((p) => p.damage_in_round !== undefined)}
<td class="font-mono">{player.damage_in_round || 0}</td> <td class="px-3 py-2 font-mono text-white/80"
>{player.damage_in_round || 0}</td
>
{/if} {/if}
</tr> </tr>
{/each} {/each}

View File

@@ -56,7 +56,7 @@
display: true, display: true,
position: 'top', position: 'top',
labels: { labels: {
color: 'rgb(156, 163, 175)', color: 'rgba(255, 255, 255, 0.7)',
font: { font: {
family: 'Inter, system-ui, sans-serif', family: 'Inter, system-ui, sans-serif',
size: 12 size: 12
@@ -64,21 +64,21 @@
} }
}, },
tooltip: { tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)', backgroundColor: 'rgba(18, 18, 26, 0.95)',
padding: 12, padding: 12,
titleColor: '#fff', titleColor: '#fff',
bodyColor: '#fff', bodyColor: 'rgba(255, 255, 255, 0.8)',
borderColor: 'rgba(255, 255, 255, 0.1)', borderColor: 'rgba(0, 212, 255, 0.3)',
borderWidth: 1 borderWidth: 1
} }
}, },
scales: { scales: {
x: { x: {
grid: { grid: {
color: 'rgba(156, 163, 175, 0.1)' color: 'rgba(255, 255, 255, 0.05)'
}, },
ticks: { ticks: {
color: 'rgb(156, 163, 175)', color: 'rgba(255, 255, 255, 0.5)',
font: { font: {
size: 11 size: 11
} }
@@ -86,10 +86,10 @@
}, },
y: { y: {
grid: { grid: {
color: 'rgba(156, 163, 175, 0.1)' color: 'rgba(255, 255, 255, 0.05)'
}, },
ticks: { ticks: {
color: 'rgb(156, 163, 175)', color: 'rgba(255, 255, 255, 0.5)',
font: { font: {
size: 11 size: 11
} }

View File

@@ -65,7 +65,7 @@
display: true, display: true,
position: 'top', position: 'top',
labels: { labels: {
color: 'rgb(156, 163, 175)', color: 'rgba(255, 255, 255, 0.7)',
font: { font: {
family: 'Inter, system-ui, sans-serif', family: 'Inter, system-ui, sans-serif',
size: 12 size: 12
@@ -73,21 +73,21 @@
} }
}, },
tooltip: { tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)', backgroundColor: 'rgba(18, 18, 26, 0.95)',
padding: 12, padding: 12,
titleColor: '#fff', titleColor: '#fff',
bodyColor: '#fff', bodyColor: 'rgba(255, 255, 255, 0.8)',
borderColor: 'rgba(255, 255, 255, 0.1)', borderColor: 'rgba(0, 212, 255, 0.3)',
borderWidth: 1 borderWidth: 1
} }
}, },
scales: { scales: {
x: { x: {
grid: { grid: {
color: 'rgba(156, 163, 175, 0.1)' color: 'rgba(255, 255, 255, 0.05)'
}, },
ticks: { ticks: {
color: 'rgb(156, 163, 175)', color: 'rgba(255, 255, 255, 0.5)',
font: { font: {
size: 11 size: 11
} }
@@ -95,10 +95,10 @@
}, },
y: { y: {
grid: { grid: {
color: 'rgba(156, 163, 175, 0.1)' color: 'rgba(255, 255, 255, 0.05)'
}, },
ticks: { ticks: {
color: 'rgb(156, 163, 175)', color: 'rgba(255, 255, 255, 0.5)',
font: { font: {
size: 11 size: 11
} }

View File

@@ -54,7 +54,7 @@
display: true, display: true,
position: 'bottom', position: 'bottom',
labels: { labels: {
color: 'rgb(156, 163, 175)', color: 'rgba(255, 255, 255, 0.7)',
font: { font: {
family: 'Inter, system-ui, sans-serif', family: 'Inter, system-ui, sans-serif',
size: 12 size: 12
@@ -63,11 +63,11 @@
} }
}, },
tooltip: { tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)', backgroundColor: 'rgba(18, 18, 26, 0.95)',
padding: 12, padding: 12,
titleColor: '#fff', titleColor: '#fff',
bodyColor: '#fff', bodyColor: 'rgba(255, 255, 255, 0.8)',
borderColor: 'rgba(255, 255, 255, 0.1)', borderColor: 'rgba(0, 212, 255, 0.3)',
borderWidth: 1 borderWidth: 1
} }
} }

View File

@@ -17,13 +17,13 @@
children children
}: Props = $props(); }: Props = $props();
const baseClasses = 'bg-base-200 border border-base-300 rounded-md transition-all duration-200'; const baseClasses = 'bg-void-light border border-white/10 rounded-xl transition-all duration-300';
const variantClasses = { const variantClasses = {
default: 'shadow-sm', default: 'shadow-sm hover:shadow-[0_0_20px_rgba(0,212,255,0.05)]',
elevated: 'shadow-lg shadow-black/10', elevated: 'shadow-lg shadow-black/20 hover:shadow-[0_0_30px_rgba(0,212,255,0.1)]',
interactive: interactive:
'cursor-pointer hover:border-primary hover:shadow-lg hover:shadow-primary/20 hover:-translate-y-0.5' 'cursor-pointer hover:border-neon-blue/50 hover:shadow-[0_0_20px_rgba(0,212,255,0.15)] hover:-translate-y-0.5'
}; };
const paddingClasses = { const paddingClasses = {

View File

@@ -12,8 +12,7 @@
tabs: Tab[]; tabs: Tab[];
activeTab?: string; activeTab?: string;
onTabChange?: (value: string) => void; onTabChange?: (value: string) => void;
variant?: 'boxed' | 'bordered' | 'lifted'; size?: 'sm' | 'md' | 'lg';
size?: 'xs' | 'sm' | 'md' | 'lg';
class?: string; class?: string;
} }
@@ -21,7 +20,6 @@
tabs, tabs,
activeTab = $bindable(), activeTab = $bindable(),
onTabChange, onTabChange,
variant = 'bordered',
size = 'md', size = 'md',
class: className = '' class: className = ''
}: Props = $props(); }: Props = $props();
@@ -43,21 +41,36 @@
} }
}; };
const variantClass = const sizeClasses = {
variant === 'boxed' ? 'tabs-boxed' : variant === 'lifted' ? 'tabs-lifted' : ''; sm: 'text-xs px-3 py-1.5',
const sizeClass = md: 'text-sm px-4 py-2',
size === 'xs' ? 'tabs-xs' : size === 'sm' ? 'tabs-sm' : size === 'lg' ? 'tabs-lg' : ''; lg: 'text-base px-5 py-2.5'
};
const baseTabClasses = 'rounded-md font-medium transition-all duration-200 whitespace-nowrap';
const inactiveClasses = 'text-white/60 hover:text-white hover:bg-white/5';
const activeClasses =
'text-neon-blue bg-neon-blue/10 border border-neon-blue/50 shadow-[0_0_10px_rgba(0,212,255,0.15)]';
const disabledClasses = 'opacity-40 cursor-not-allowed pointer-events-none';
</script> </script>
<div role="tablist" class="tabs {variantClass} {sizeClass} {className}"> <div
role="tablist"
class="inline-flex gap-1 rounded-lg bg-void/50 p-1 backdrop-blur-sm {className}"
>
{#each tabs as tab} {#each tabs as tab}
{@const active = isActive(tab)}
{@const classes = `${baseTabClasses} ${sizeClasses[size]} ${active ? activeClasses : inactiveClasses} ${tab.disabled ? disabledClasses : ''}`}
{#if tab.href} {#if tab.href}
<a <a
href={tab.href} href={tab.href}
role="tab" role="tab"
class="tab" class={classes}
class:tab-active={isActive(tab)} aria-selected={active}
class:tab-disabled={tab.disabled}
aria-disabled={tab.disabled} aria-disabled={tab.disabled}
> >
{tab.label} {tab.label}
@@ -65,9 +78,8 @@
{:else} {:else}
<button <button
role="tab" role="tab"
class="tab" class={classes}
class:tab-active={isActive(tab)} aria-selected={active}
class:tab-disabled={tab.disabled}
disabled={tab.disabled} disabled={tab.disabled}
onclick={() => handleTabClick(tab)} onclick={() => handleTabClick(tab)}
> >

View File

@@ -11,7 +11,6 @@
const { match } = data; const { match } = data;
function handleBack() { function handleBack() {
// Navigate back to matches page
goto('/matches'); goto('/matches');
} }
@@ -47,21 +46,31 @@
alert('Share code not available for this match'); alert('Share code not available for this match');
return; return;
} }
// Open the demo download URL (typically from Valve servers or cached location)
// Format: steam://rungame/730/76561202255233023/+csgo_download_match%20{SHARE_CODE}
const downloadUrl = `steam://rungame/730/76561202255233023/+csgo_download_match%20${match.share_code}`; const downloadUrl = `steam://rungame/730/76561202255233023/+csgo_download_match%20${match.share_code}`;
window.location.href = downloadUrl; window.location.href = downloadUrl;
} }
</script> </script>
<!-- Match Header with Background --> <!-- Match Header with Background -->
<div class="relative overflow-hidden border-b border-base-300"> <div class="relative overflow-hidden border-b border-neon-blue/20 bg-void">
<!-- Background Image --> <!-- Background Image -->
<div class="absolute inset-0"> <div class="absolute inset-0">
<img src={mapBg} alt={mapName} class="h-full w-full object-cover" onerror={handleImageError} /> <img src={mapBg} alt={mapName} class="h-full w-full object-cover" onerror={handleImageError} />
<!-- Multi-layer gradient overlay for depth and framing --> <!-- Multi-layer gradient overlay for depth -->
<div class="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-black/40"></div> <div class="absolute inset-0 bg-gradient-to-b from-void/80 via-transparent to-void"></div>
<div class="absolute inset-0 bg-gradient-to-r from-black/70 via-black/40 to-black/70"></div> <div class="absolute inset-0 bg-gradient-to-r from-void/70 via-void/30 to-void/70"></div>
</div>
<!-- Decorative Neon Blur Orbs -->
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="absolute -left-32 top-10 h-64 w-64 rounded-full bg-neon-blue/20 blur-[100px]"></div>
<div
class="absolute -right-32 top-20 h-64 w-64 rounded-full bg-neon-gold/15 blur-[100px]"
></div>
<div
class="absolute inset-0 opacity-10"
style="background-image: linear-gradient(rgba(0, 212, 255, 0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 212, 255, 0.05) 1px, transparent 1px); background-size: 50px 50px;"
></div>
</div> </div>
<div class="container relative mx-auto px-4 py-8"> <div class="container relative mx-auto px-4 py-8">
@@ -69,7 +78,7 @@
<div class="mb-4"> <div class="mb-4">
<button <button
onclick={handleBack} onclick={handleBack}
class="btn btn-sm gap-2 bg-black/60 text-white backdrop-blur-sm hover:bg-black/80" class="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-void/60 px-3 py-2 text-sm text-white/80 backdrop-blur-sm transition-all duration-200 hover:border-neon-blue/30 hover:bg-void/80 hover:text-white"
> >
<ArrowLeft class="h-4 w-4" /> <ArrowLeft class="h-4 w-4" />
<span>Back to Matches</span> <span>Back to Matches</span>
@@ -79,14 +88,17 @@
<!-- Map Name --> <!-- Map Name -->
<div class="mb-6 flex items-center justify-between"> <div class="mb-6 flex items-center justify-between">
<div> <div>
<h1 class="text-5xl font-bold text-white drop-shadow-2xl"> <h1
class="text-5xl font-bold text-white"
style="text-shadow: 0 0 40px rgba(0, 212, 255, 0.3), 0 4px 20px rgba(0, 0, 0, 0.5);"
>
{mapName} {mapName}
</h1> </h1>
</div> </div>
{#if match.demo_parsed && match.share_code} {#if match.demo_parsed && match.share_code}
<button <button
onclick={handleDownloadDemo} onclick={handleDownloadDemo}
class="btn btn-ghost gap-2 border border-white/25 bg-white/15 text-white backdrop-blur-md hover:bg-white/25" class="inline-flex items-center gap-2 rounded-lg border border-neon-blue/30 bg-neon-blue/10 px-4 py-2 text-sm font-medium text-neon-blue backdrop-blur-md transition-all duration-200 hover:border-neon-blue/50 hover:bg-neon-blue/20 hover:shadow-[0_0_20px_rgba(0,212,255,0.2)]"
title="Download this match demo to your Steam client" title="Download this match demo to your Steam client"
> >
<Download class="h-4 w-4" /> <Download class="h-4 w-4" />
@@ -95,59 +107,67 @@
{/if} {/if}
</div> </div>
<!-- Hero Info Panel with translucent background --> <!-- Hero Info Panel -->
<div <div
class="mx-auto max-w-3xl rounded-xl border border-white/10 bg-black/40 p-6 backdrop-blur-md" class="mx-auto max-w-3xl rounded-xl border border-white/10 bg-void/60 p-6 backdrop-blur-xl"
> >
<!-- Score --> <!-- Score -->
<div class="mb-4 flex items-center justify-center gap-8"> <div class="mb-4 flex items-center justify-center gap-8">
<div class="text-center"> <div class="text-center">
<div class="mb-1 text-xs font-medium uppercase tracking-wider text-white/70"> <div class="mb-1 text-xs font-medium uppercase tracking-wider text-white/60">
Terrorists Terrorists
</div> </div>
<div class="font-mono text-6xl font-bold text-terrorist drop-shadow-lg"> <div
class="font-mono text-6xl font-bold text-terrorist"
style="text-shadow: 0 0 30px rgba(212, 167, 74, 0.5);"
>
{match.score_team_a} {match.score_team_a}
</div> </div>
</div> </div>
<div class="text-4xl font-bold text-white/50">:</div> <div class="text-4xl font-bold text-white/30">:</div>
<div class="text-center"> <div class="text-center">
<div class="mb-1 text-xs font-medium uppercase tracking-wider text-white/70"> <div class="mb-1 text-xs font-medium uppercase tracking-wider text-white/60">
Counter-Terrorists Counter-Terrorists
</div> </div>
<div class="font-mono text-6xl font-bold text-ct drop-shadow-lg"> <div
class="font-mono text-6xl font-bold text-ct"
style="text-shadow: 0 0 30px rgba(94, 152, 217, 0.5);"
>
{match.score_team_b} {match.score_team_b}
</div> </div>
</div> </div>
</div> </div>
<!-- Match Meta --> <!-- Match Meta -->
<div class="flex flex-wrap items-center justify-center gap-3 text-sm text-white/90"> <div class="flex flex-wrap items-center justify-center gap-3 text-sm text-white/70">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<Calendar class="h-3.5 w-3.5" /> <Calendar class="h-3.5 w-3.5 text-neon-blue" />
<span>{formattedDate}</span> <span>{formattedDate}</span>
</div> </div>
<span class="text-white/30"></span> <span class="text-white/20"></span>
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<Clock class="h-3.5 w-3.5" /> <Clock class="h-3.5 w-3.5 text-neon-blue" />
<span>{duration}</span> <span>{duration}</span>
</div> </div>
<span class="text-white/30"></span> <span class="text-white/20"></span>
<span>MR12 ({match.max_rounds} rounds)</span> <span>MR12 ({match.max_rounds} rounds)</span>
{#if match.demo_parsed} {#if match.demo_parsed}
<span class="text-white/30"></span> <span class="text-white/20"></span>
<Badge variant="success" size="sm">Demo Parsed</Badge> <Badge variant="success" size="sm">Demo Parsed</Badge>
{/if} {/if}
</div> </div>
</div> </div>
<!-- Tabs --> <!-- Tabs -->
<div class="mt-6 rounded-lg border border-white/10 bg-black/35 p-4 backdrop-blur-lg"> <div class="mt-6 flex justify-center">
<Tabs {tabs} variant="bordered" size="md" /> <Tabs {tabs} size="md" class="border border-white/10" />
</div> </div>
</div> </div>
</div> </div>
<!-- Tab Content --> <!-- Tab Content -->
<div class="container mx-auto px-4 py-8"> <div class="min-h-screen bg-void">
{@render children()} <div class="container mx-auto px-4 py-8">
{@render children()}
</div>
</div> </div>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Trophy } from 'lucide-svelte'; import { Trophy, Zap } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte'; import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte'; import Badge from '$lib/components/ui/Badge.svelte';
import PremierRatingBadge from '$lib/components/ui/PremierRatingBadge.svelte'; import PremierRatingBadge from '$lib/components/ui/PremierRatingBadge.svelte';
@@ -51,12 +51,16 @@
const teamAStats = calcTeamStats(sortedTeamA); const teamAStats = calcTeamStats(sortedTeamA);
const teamBStats = calcTeamStats(sortedTeamB); const teamBStats = calcTeamStats(sortedTeamB);
// Find the overall MVP (highest kills)
const allPlayers = [...sortedTeamA, ...sortedTeamB].sort((a, b) => b.kills - a.kills);
const mvpPlayerId = allPlayers[0]?.id;
</script> </script>
<div class="space-y-8"> <div class="space-y-8">
<!-- Team Statistics Overview --> <!-- Team Statistics Overview -->
<div class="grid gap-6 md:grid-cols-2"> <div class="grid gap-6 md:grid-cols-2">
<Card padding="lg"> <Card padding="lg" class="border-l-4 border-l-terrorist">
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<h2 class="text-2xl font-bold text-terrorist">Terrorists</h2> <h2 class="text-2xl font-bold text-terrorist">Terrorists</h2>
@@ -64,29 +68,34 @@
<PremierRatingBadge rating={teamAStats.avgRating} {match} size="sm" showIcon={true} /> <PremierRatingBadge rating={teamAStats.avgRating} {match} size="sm" showIcon={true} />
{/if} {/if}
</div> </div>
<div class="font-mono text-3xl font-bold text-terrorist">{match.score_team_a}</div> <div
class="font-mono text-3xl font-bold text-terrorist"
style="text-shadow: 0 0 20px rgba(212, 167, 74, 0.4);"
>
{match.score_team_a}
</div>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<div class="text-sm text-base-content/60">Team K/D</div> <div class="text-sm text-white/50">Team K/D</div>
<div class="text-xl font-bold">{teamAStats.kd}</div> <div class="text-xl font-bold text-white">{teamAStats.kd}</div>
</div> </div>
<div> <div>
<div class="text-sm text-base-content/60">Avg ADR</div> <div class="text-sm text-white/50">Avg ADR</div>
<div class="text-xl font-bold">{teamAStats.adr}</div> <div class="text-xl font-bold text-white">{teamAStats.adr}</div>
</div> </div>
<div> <div>
<div class="text-sm text-base-content/60">Total Kills</div> <div class="text-sm text-white/50">Total Kills</div>
<div class="text-xl font-bold">{teamAStats.kills}</div> <div class="text-xl font-bold text-white">{teamAStats.kills}</div>
</div> </div>
<div> <div>
<div class="text-sm text-base-content/60">Avg KAST</div> <div class="text-sm text-white/50">Avg KAST</div>
<div class="text-xl font-bold">{teamAStats.kast}%</div> <div class="text-xl font-bold text-white">{teamAStats.kast}%</div>
</div> </div>
</div> </div>
</Card> </Card>
<Card padding="lg"> <Card padding="lg" class="border-l-4 border-l-ct">
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<h2 class="text-2xl font-bold text-ct">Counter-Terrorists</h2> <h2 class="text-2xl font-bold text-ct">Counter-Terrorists</h2>
@@ -94,24 +103,29 @@
<PremierRatingBadge rating={teamBStats.avgRating} {match} size="sm" showIcon={true} /> <PremierRatingBadge rating={teamBStats.avgRating} {match} size="sm" showIcon={true} />
{/if} {/if}
</div> </div>
<div class="font-mono text-3xl font-bold text-ct">{match.score_team_b}</div> <div
class="font-mono text-3xl font-bold text-ct"
style="text-shadow: 0 0 20px rgba(94, 152, 217, 0.4);"
>
{match.score_team_b}
</div>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<div class="text-sm text-base-content/60">Team K/D</div> <div class="text-sm text-white/50">Team K/D</div>
<div class="text-xl font-bold">{teamBStats.kd}</div> <div class="text-xl font-bold text-white">{teamBStats.kd}</div>
</div> </div>
<div> <div>
<div class="text-sm text-base-content/60">Avg ADR</div> <div class="text-sm text-white/50">Avg ADR</div>
<div class="text-xl font-bold">{teamBStats.adr}</div> <div class="text-xl font-bold text-white">{teamBStats.adr}</div>
</div> </div>
<div> <div>
<div class="text-sm text-base-content/60">Total Kills</div> <div class="text-sm text-white/50">Total Kills</div>
<div class="text-xl font-bold">{teamBStats.kills}</div> <div class="text-xl font-bold text-white">{teamBStats.kills}</div>
</div> </div>
<div> <div>
<div class="text-sm text-base-content/60">Avg KAST</div> <div class="text-sm text-white/50">Avg KAST</div>
<div class="text-xl font-bold">{teamBStats.kast}%</div> <div class="text-xl font-bold text-white">{teamBStats.kast}%</div>
</div> </div>
</div> </div>
</Card> </Card>
@@ -120,49 +134,61 @@
<!-- Scoreboard --> <!-- Scoreboard -->
<Card padding="none"> <Card padding="none">
<div class="p-6"> <div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Scoreboard</h2> <h2 class="text-2xl font-bold text-white">Scoreboard</h2>
</div> </div>
<!-- Team A --> <!-- Team A -->
<div class="border-t border-base-300 bg-terrorist/5"> <div class="border-t border-white/10 bg-terrorist/5">
<div class="px-6 py-3"> <div class="flex items-center justify-between px-6 py-3">
<h3 class="text-lg font-semibold text-terrorist">Terrorists</h3> <h3 class="text-lg font-semibold text-terrorist">Terrorists</h3>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table" style="table-layout: fixed;"> <table class="w-full" style="table-layout: fixed;">
<thead> <thead>
<tr class="border-base-300"> <tr class="border-b border-white/10 bg-void/50 text-left text-sm text-white/50">
<th style="width: 200px;">Player</th> <th class="px-6 py-3 font-medium" style="width: 200px;">Player</th>
<th style="width: 80px;">K</th> <th class="px-4 py-3 font-medium" style="width: 80px;">K</th>
<th style="width: 80px;">D</th> <th class="px-4 py-3 font-medium" style="width: 80px;">D</th>
<th style="width: 80px;">A</th> <th class="px-4 py-3 font-medium" style="width: 80px;">A</th>
<th style="width: 100px;">ADR</th> <th class="px-4 py-3 font-medium" style="width: 100px;">ADR</th>
<th style="width: 100px;">HS%</th> <th class="px-4 py-3 font-medium" style="width: 100px;">HS%</th>
<th style="width: 100px;">KAST%</th> <th class="px-4 py-3 font-medium" style="width: 100px;">KAST%</th>
<th style="width: 180px;">Rating</th> <th class="px-4 py-3 font-medium" style="width: 180px;">Rating</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each sortedTeamA as player, index} {#each sortedTeamA as player, index}
<tr class="border-base-300"> <tr class="border-b border-white/5 transition-colors hover:bg-neon-blue/5">
<td> <td class="px-6 py-3">
<a <a
href={`/player/${player.id}`} href={`/player/${player.id}`}
class="font-medium transition-colors hover:text-primary" class="font-medium text-white transition-colors hover:text-neon-blue"
> >
{player.name} {player.name}
</a> </a>
{#if index === 0} {#if player.id === mvpPlayerId}
<Trophy class="ml-2 inline h-4 w-4 text-warning" /> <span
class="ml-2 inline-flex items-center gap-1 rounded-full bg-neon-gold/20 px-2 py-0.5 text-xs font-medium text-neon-gold"
title="Most Violent Player"
>
<Zap class="h-3 w-3" />
MVP
</span>
{:else if index === 0}
<Trophy class="ml-2 inline h-4 w-4 text-terrorist" />
{/if} {/if}
</td> </td>
<td class="font-mono font-semibold">{player.kills}</td> <td class="px-4 py-3 font-mono font-semibold text-white">{player.kills}</td>
<td class="font-mono">{player.deaths}</td> <td class="px-4 py-3 font-mono text-white/80">{player.deaths}</td>
<td class="font-mono">{player.assists}</td> <td class="px-4 py-3 font-mono text-white/80">{player.assists}</td>
<td class="font-mono">{(player.adr || 0).toFixed(1)}</td> <td class="px-4 py-3 font-mono text-white/80">{(player.adr || 0).toFixed(1)}</td>
<td class="font-mono">{(player.hs_percent || 0).toFixed(1)}%</td> <td class="px-4 py-3 font-mono text-white/80"
<td class="font-mono">{player.kast?.toFixed(1) || '0.0'}%</td> >{(player.hs_percent || 0).toFixed(1)}%</td
<td class="h-12"> >
<td class="px-4 py-3 font-mono text-white/80"
>{player.kast?.toFixed(1) || '0.0'}%</td
>
<td class="h-12 px-4 py-3">
<div class="flex h-full items-center"> <div class="flex h-full items-center">
<PremierRatingBadge <PremierRatingBadge
rating={player.rank_new} rating={player.rank_new}
@@ -181,45 +207,57 @@
</div> </div>
<!-- Team B --> <!-- Team B -->
<div class="border-t border-base-300 bg-ct/5"> <div class="border-t border-white/10 bg-ct/5">
<div class="px-6 py-3"> <div class="flex items-center justify-between px-6 py-3">
<h3 class="text-lg font-semibold text-ct">Counter-Terrorists</h3> <h3 class="text-lg font-semibold text-ct">Counter-Terrorists</h3>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table" style="table-layout: fixed;"> <table class="w-full" style="table-layout: fixed;">
<thead> <thead>
<tr class="border-base-300"> <tr class="border-b border-white/10 bg-void/50 text-left text-sm text-white/50">
<th style="width: 200px;">Player</th> <th class="px-6 py-3 font-medium" style="width: 200px;">Player</th>
<th style="width: 80px;">K</th> <th class="px-4 py-3 font-medium" style="width: 80px;">K</th>
<th style="width: 80px;">D</th> <th class="px-4 py-3 font-medium" style="width: 80px;">D</th>
<th style="width: 80px;">A</th> <th class="px-4 py-3 font-medium" style="width: 80px;">A</th>
<th style="width: 100px;">ADR</th> <th class="px-4 py-3 font-medium" style="width: 100px;">ADR</th>
<th style="width: 100px;">HS%</th> <th class="px-4 py-3 font-medium" style="width: 100px;">HS%</th>
<th style="width: 100px;">KAST%</th> <th class="px-4 py-3 font-medium" style="width: 100px;">KAST%</th>
<th style="width: 180px;">Rating</th> <th class="px-4 py-3 font-medium" style="width: 180px;">Rating</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each sortedTeamB as player, index} {#each sortedTeamB as player, index}
<tr class="border-base-300"> <tr class="border-b border-white/5 transition-colors hover:bg-neon-blue/5">
<td> <td class="px-6 py-3">
<a <a
href={`/player/${player.id}`} href={`/player/${player.id}`}
class="font-medium transition-colors hover:text-primary" class="font-medium text-white transition-colors hover:text-neon-blue"
> >
{player.name} {player.name}
</a> </a>
{#if index === 0} {#if player.id === mvpPlayerId}
<Trophy class="ml-2 inline h-4 w-4 text-warning" /> <span
class="ml-2 inline-flex items-center gap-1 rounded-full bg-neon-gold/20 px-2 py-0.5 text-xs font-medium text-neon-gold"
title="Most Violent Player"
>
<Zap class="h-3 w-3" />
MVP
</span>
{:else if index === 0}
<Trophy class="ml-2 inline h-4 w-4 text-ct" />
{/if} {/if}
</td> </td>
<td class="font-mono font-semibold">{player.kills}</td> <td class="px-4 py-3 font-mono font-semibold text-white">{player.kills}</td>
<td class="font-mono">{player.deaths}</td> <td class="px-4 py-3 font-mono text-white/80">{player.deaths}</td>
<td class="font-mono">{player.assists}</td> <td class="px-4 py-3 font-mono text-white/80">{player.assists}</td>
<td class="font-mono">{(player.adr || 0).toFixed(1)}</td> <td class="px-4 py-3 font-mono text-white/80">{(player.adr || 0).toFixed(1)}</td>
<td class="font-mono">{(player.hs_percent || 0).toFixed(1)}%</td> <td class="px-4 py-3 font-mono text-white/80"
<td class="font-mono">{player.kast?.toFixed(1) || '0.0'}%</td> >{(player.hs_percent || 0).toFixed(1)}%</td
<td class="h-12"> >
<td class="px-4 py-3 font-mono text-white/80"
>{player.kast?.toFixed(1) || '0.0'}%</td
>
<td class="h-12 px-4 py-3">
<div class="flex h-full items-center"> <div class="flex h-full items-center">
<PremierRatingBadge <PremierRatingBadge
rating={player.rank_new} rating={player.rank_new}
@@ -244,10 +282,13 @@
{:else} {:else}
<Card padding="lg"> <Card padding="lg">
<div class="text-center"> <div class="text-center">
<h3 class="mb-2 text-xl font-semibold text-base-content">Round Timeline</h3> <h3 class="mb-2 text-xl font-semibold text-white">Round Timeline</h3>
<p class="text-base-content/60"> <p class="text-white/60">
Round-by-round timeline data is not available for this match. This requires the demo to be {#if !match.demo_parsed}
fully 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> </p>
{#if !match.demo_parsed} {#if !match.demo_parsed}
<Badge variant="warning" size="md" class="mt-4">Demo Not Yet Parsed</Badge> <Badge variant="warning" size="md" class="mt-4">Demo Not Yet Parsed</Badge>

View File

@@ -1,5 +1,12 @@
<script lang="ts"> <script lang="ts">
import { MessageSquare, Filter, Search, AlertCircle, Languages } from 'lucide-svelte'; import {
MessageSquare,
Filter,
Search,
AlertCircle,
Languages,
MessageCircle
} from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte'; import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte'; import Badge from '$lib/components/ui/Badge.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -15,7 +22,6 @@
// Check if text likely needs translation (contains non-ASCII or Cyrillic characters) // Check if text likely needs translation (contains non-ASCII or Cyrillic characters)
const mightNeedTranslation = (text: string): boolean => { const mightNeedTranslation = (text: string): boolean => {
// Check for Cyrillic, Chinese, Japanese, Korean, Arabic, etc.
const nonEnglishPattern = const nonEnglishPattern =
/[\u0400-\u04FF\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF\u0600-\u06FF]/; /[\u0400-\u04FF\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF\u0600-\u06FF]/;
return nonEnglishPattern.test(text); return nonEnglishPattern.test(text);
@@ -24,12 +30,11 @@
// Open Google Translate for a message // Open Google Translate for a message
const translateMessage = (text: string) => { const translateMessage = (text: string) => {
const encodedText = encodeURIComponent(text); const encodedText = encodeURIComponent(text);
// Use Google Translate web interface (auto-detect language to English)
const translateUrl = `https://translate.google.com/?sl=auto&tl=en&text=${encodedText}&op=translate`; const translateUrl = `https://translate.google.com/?sl=auto&tl=en&text=${encodedText}&op=translate`;
window.open(translateUrl, '_blank', 'width=800,height=600,noopener,noreferrer'); window.open(translateUrl, '_blank', 'width=800,height=600,noopener,noreferrer');
}; };
// Get unique players who sent messages - use $derived for computed values // Get unique players who sent messages
const messagePlayers = $derived( const messagePlayers = $derived(
chatData chatData
? Array.from(new Set(chatData.messages.map((m) => m.player_id))) ? Array.from(new Set(chatData.messages.map((m) => m.player_id)))
@@ -49,18 +54,12 @@
const filteredMessages = $derived( const filteredMessages = $derived(
chatData chatData
? chatData.messages.filter((msg) => { ? chatData.messages.filter((msg) => {
// Chat type filter
if (!showTeamChat && !msg.all_chat) return false; if (!showTeamChat && !msg.all_chat) return false;
if (!showAllChat && msg.all_chat) return false; if (!showAllChat && msg.all_chat) return false;
// Player filter
if (selectedPlayer !== null && msg.player_id !== selectedPlayer) return false; if (selectedPlayer !== null && msg.player_id !== selectedPlayer) return false;
// Search filter
if (searchQuery && !msg.message.toLowerCase().includes(searchQuery.toLowerCase())) { if (searchQuery && !msg.message.toLowerCase().includes(searchQuery.toLowerCase())) {
return false; return false;
} }
return true; return true;
}) })
: [] : []
@@ -100,40 +99,72 @@
{#if !chatData} {#if !chatData}
<Card padding="lg"> <Card padding="lg">
<div class="text-center"> <div class="text-center">
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" /> <AlertCircle
<h2 class="mb-2 text-2xl font-bold text-base-content">Match Not Parsed</h2> class="mx-auto mb-4 h-16 w-16 text-neon-gold"
<p class="mb-4 text-base-content/60"> style="filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.4));"
/>
<h2 class="mb-2 text-2xl font-bold text-white">Match Not Parsed</h2>
<p class="mb-4 text-white/60">
This match hasn't been parsed yet, so chat data is not available. This match hasn't been parsed yet, so chat data is not available.
</p> </p>
<Badge variant="warning" size="lg">Demo parsing required</Badge> <Badge variant="warning" size="lg">Demo parsing required</Badge>
</div> </div>
</Card> </Card>
{:else if totalMessages === 0}
<Card padding="lg">
<div class="text-center">
<MessageCircle class="mx-auto mb-4 h-16 w-16 text-white/30" />
<h2 class="mb-2 text-2xl font-bold text-white">No Chat Messages</h2>
<p class="mb-4 text-white/60">No comms? Either tactical geniuses or solo queue...</p>
</div>
</Card>
{:else} {:else}
<div class="space-y-6"> <div class="space-y-6">
<!-- Stats --> <!-- Stats -->
<div class="grid gap-6 md:grid-cols-3"> <div class="grid gap-6 md:grid-cols-3">
<Card padding="lg"> <Card padding="lg">
<div class="mb-2 flex items-center gap-2"> <div class="flex items-center gap-3">
<MessageSquare class="h-5 w-5 text-primary" /> <div
<span class="text-sm font-medium text-base-content/70">Total Messages</span> 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);"
>
<MessageSquare class="h-5 w-5 text-neon-blue" />
</div>
<div>
<div class="text-sm text-white/50">Total Messages</div>
<div class="text-3xl font-bold text-white">{totalMessages}</div>
</div>
</div> </div>
<div class="text-3xl font-bold text-base-content">{totalMessages}</div>
</Card> </Card>
<Card padding="lg"> <Card padding="lg">
<div class="mb-2 flex items-center gap-2"> <div class="flex items-center gap-3">
<MessageSquare class="h-5 w-5 text-warning" /> <div
<span class="text-sm font-medium text-base-content/70">Team Chat</span> 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);"
>
<MessageSquare class="h-5 w-5 text-neon-gold" />
</div>
<div>
<div class="text-sm text-white/50">Team Chat</div>
<div class="text-3xl font-bold text-white">{teamChatCount}</div>
</div>
</div> </div>
<div class="text-3xl font-bold text-base-content">{teamChatCount}</div>
</Card> </Card>
<Card padding="lg"> <Card padding="lg">
<div class="mb-2 flex items-center gap-2"> <div class="flex items-center gap-3">
<MessageSquare class="h-5 w-5 text-success" /> <div
<span class="text-sm font-medium text-base-content/70">All Chat</span> 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);"
>
<MessageSquare class="h-5 w-5 text-neon-green" />
</div>
<div>
<div class="text-sm text-white/50">All Chat</div>
<div class="text-3xl font-bold text-white">{allChatCount}</div>
</div>
</div> </div>
<div class="text-3xl font-bold text-base-content">{allChatCount}</div>
</Card> </Card>
</div> </div>
@@ -141,25 +172,36 @@
<Card padding="lg"> <Card padding="lg">
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Filter class="h-5 w-5 text-base-content" /> <Filter class="h-5 w-5 text-neon-blue" />
<h3 class="font-semibold">Filters</h3> <h3 class="font-semibold text-white">Filters</h3>
</div> </div>
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
<!-- Chat Type --> <!-- Chat Type -->
<div class="flex gap-2"> <div class="flex gap-4">
<label class="label cursor-pointer gap-2"> <label class="flex cursor-pointer items-center gap-2">
<input type="checkbox" bind:checked={showTeamChat} class="checkbox checkbox-sm" /> <input
<span class="label-text">Team Chat</span> type="checkbox"
bind:checked={showTeamChat}
class="h-4 w-4 rounded border-white/20 bg-void text-neon-blue focus:ring-neon-blue/50"
/>
<span class="text-sm text-white/80">Team Chat</span>
</label> </label>
<label class="label cursor-pointer gap-2"> <label class="flex cursor-pointer items-center gap-2">
<input type="checkbox" bind:checked={showAllChat} class="checkbox checkbox-sm" /> <input
<span class="label-text">All Chat</span> type="checkbox"
bind:checked={showAllChat}
class="h-4 w-4 rounded border-white/20 bg-void text-neon-blue focus:ring-neon-blue/50"
/>
<span class="text-sm text-white/80">All Chat</span>
</label> </label>
</div> </div>
<!-- Player Filter --> <!-- Player Filter -->
<select bind:value={selectedPlayer} class="select select-bordered select-sm"> <select
bind:value={selectedPlayer}
class="rounded-lg border border-white/10 bg-void px-3 py-1.5 text-sm text-white focus:border-neon-blue/50 focus:outline-none focus:ring-1 focus:ring-neon-blue/50"
>
<option value={null}>All Players</option> <option value={null}>All Players</option>
{#each messagePlayers as player} {#each messagePlayers as player}
<option value={player.id}>{player.name}</option> <option value={player.id}>{player.name}</option>
@@ -168,12 +210,12 @@
<!-- Search --> <!-- Search -->
<div class="relative min-w-[200px] flex-1"> <div class="relative min-w-[200px] flex-1">
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-base-content/40" /> <Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-white/40" />
<input <input
type="text" type="text"
bind:value={searchQuery} bind:value={searchQuery}
placeholder="Search messages..." placeholder="Search messages..."
class="input input-sm input-bordered w-full pl-9" class="w-full rounded-lg border border-white/10 bg-void py-1.5 pl-9 pr-3 text-sm text-white placeholder-white/40 focus:border-neon-blue/50 focus:outline-none focus:ring-1 focus:ring-neon-blue/50"
/> />
</div> </div>
</div> </div>
@@ -183,7 +225,7 @@
<!-- Messages --> <!-- Messages -->
{#if filteredMessages.length === 0} {#if filteredMessages.length === 0}
<Card padding="lg"> <Card padding="lg">
<div class="text-center text-base-content/60"> <div class="text-center text-white/50">
<MessageSquare class="mx-auto mb-2 h-12 w-12" /> <MessageSquare class="mx-auto mb-2 h-12 w-12" />
<p>No messages match your filters.</p> <p>No messages match your filters.</p>
</div> </div>
@@ -192,12 +234,14 @@
{#each rounds as round} {#each rounds as round}
<Card padding="none"> <Card padding="none">
<!-- Round Header --> <!-- Round Header -->
<div class="border-b border-base-300 bg-base-200 px-6 py-3"> <div class="border-b border-white/10 bg-void/50 px-6 py-3">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h3 class="font-semibold text-base-content"> <h3 class="font-semibold text-white">
{round === 0 ? 'Warmup / Pre-Match' : `Round ${round}`} {round === 0 ? 'Warmup / Pre-Match' : `Round ${round}`}
</h3> </h3>
<Badge variant="default" size="sm"> <span
class="rounded-md border border-white/10 bg-white/5 px-2 py-0.5 text-xs text-white/60"
>
{messagesByRound[round] ? messagesByRound[round].length : 0} message{(messagesByRound[ {messagesByRound[round] ? messagesByRound[round].length : 0} message{(messagesByRound[
round round
] ]
@@ -205,25 +249,27 @@
: 0) !== 1 : 0) !== 1
? 's' ? 's'
: ''} : ''}
</Badge> </span>
</div> </div>
</div> </div>
<!-- Messages --> <!-- Messages -->
<div class="divide-y divide-base-300"> <div class="divide-y divide-white/5">
{#each messagesByRound[round] as message} {#each messagesByRound[round] as message}
{@const player = match.players?.find((p) => p.id === String(message.player_id))} {@const player = match.players?.find((p) => p.id === String(message.player_id))}
{@const playerName = {@const playerName =
message.player_name || player?.name || `Player ${message.player_id}`} message.player_name || player?.name || `Player ${message.player_id}`}
{@const teamId = player?.team_id || 0} {@const teamId = player?.team_id || 0}
<div class="p-4 transition-colors hover:bg-base-200/50"> <div class="p-4 transition-colors hover:bg-white/5">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<!-- Player Avatar/Icon --> <!-- Player Avatar/Icon -->
<div <div
class="flex h-10 w-10 items-center justify-center rounded-full text-sm font-bold text-white" class="flex h-10 w-10 items-center justify-center rounded-full text-sm font-bold text-white {teamId ===
class:bg-terrorist={teamId === 2} 2
class:bg-ct={teamId === 3} ? 'bg-terrorist'
class:bg-base-300={teamId === 0} : teamId === 3
? 'bg-ct'
: 'bg-white/20'}"
> >
{playerName.charAt(0).toUpperCase()} {playerName.charAt(0).toUpperCase()}
</div> </div>
@@ -233,24 +279,33 @@
<div class="flex items-baseline gap-2"> <div class="flex items-baseline gap-2">
<a <a
href={`/player/${message.player_id || 0}`} href={`/player/${message.player_id || 0}`}
class="font-semibold hover:underline" class="font-semibold transition-colors hover:text-neon-blue"
class:text-terrorist={teamId === 2} class:text-terrorist={teamId === 2}
class:text-ct={teamId === 3} class:text-ct={teamId === 3}
class:text-white={teamId === 0}
> >
{playerName} {playerName}
</a> </a>
{#if message.all_chat} {#if message.all_chat}
<Badge variant="success" size="sm">All Chat</Badge> <span
class="rounded-md border border-neon-green/30 bg-neon-green/10 px-1.5 py-0.5 text-xs text-neon-green"
>
All Chat
</span>
{:else} {:else}
<Badge variant="default" size="sm">Team</Badge> <span
class="rounded-md border border-white/20 bg-white/5 px-1.5 py-0.5 text-xs text-white/60"
>
Team
</span>
{/if} {/if}
</div> </div>
<div class="mt-1 flex items-start gap-2"> <div class="mt-1 flex items-start gap-2">
<p class="break-words text-base-content">{message.message}</p> <p class="break-words text-white/90">{message.message}</p>
{#if mightNeedTranslation(message.message)} {#if mightNeedTranslation(message.message)}
<button <button
onclick={() => translateMessage(message.message)} onclick={() => translateMessage(message.message)}
class="btn btn-ghost btn-xs flex-shrink-0 gap-1" class="flex shrink-0 items-center gap-1 rounded-md border border-neon-blue/30 bg-neon-blue/10 px-2 py-0.5 text-xs text-neon-blue transition-colors hover:bg-neon-blue/20"
title="Translate message" title="Translate message"
aria-label="Translate to English" aria-label="Translate to English"
> >

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Target, Crosshair, AlertCircle } from 'lucide-svelte'; import { Target, Crosshair, AlertCircle, Flame, Skull, Lightbulb } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte'; import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte'; import Badge from '$lib/components/ui/Badge.svelte';
import DataTable from '$lib/components/data-display/DataTable.svelte'; import DataTable from '$lib/components/data-display/DataTable.svelte';
@@ -23,8 +23,6 @@
const damage = player.dmg_enemy || 0; const damage = player.dmg_enemy || 0;
const avgDamagePerRound = match.max_rounds > 0 ? damage / match.max_rounds : 0; const avgDamagePerRound = match.max_rounds > 0 ? damage / match.max_rounds : 0;
// Note: Hit group breakdown would require weapon stats data
// For now, using total damage metrics
return { return {
...player, ...player,
damage, damage,
@@ -69,6 +67,11 @@
// Top damage dealers (top 3) // Top damage dealers (top 3)
const topDamageDealers = sortedByDamage.slice(0, 3); const topDamageDealers = sortedByDamage.slice(0, 3);
// Find player with highest team damage (needs therapy)
const needsTherapyPlayer = [...playersWithDamageStats].sort(
(a, b) => (b.dmg_team || 0) - (a.dmg_team || 0)
)[0];
// Damage table columns // Damage table columns
const damageColumns = [ const damageColumns = [
{ {
@@ -77,7 +80,7 @@
sortable: true, sortable: true,
render: (value: unknown, row: (typeof playersWithDamageStats)[0]) => { render: (value: unknown, row: (typeof playersWithDamageStats)[0]) => {
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct'; const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
return `<a href="/player/${row.id}" class="font-medium hover:underline ${teamClass}">${value}</a>`; return `<a href="/player/${row.id}" class="font-medium hover:text-neon-blue transition-colors ${teamClass}">${value}</a>`;
} }
}, },
{ {
@@ -85,15 +88,15 @@
label: 'Damage Dealt', label: 'Damage Dealt',
sortable: true, sortable: true,
align: 'right' as const, align: 'right' as const,
class: 'font-mono font-semibold', class: 'font-mono font-semibold text-white',
format: (value: unknown) => (typeof value === 'number' ? value.toLocaleString() : '0') format: (value: unknown) => (typeof value === 'number' ? value.toLocaleString() : '0')
}, },
{ {
key: 'avgDamagePerRound' as const, key: 'avgDamagePerRound' as const,
label: 'Avg Damage/Round', label: 'ADR',
sortable: true, sortable: true,
align: 'right' as const, align: 'right' as const,
class: 'font-mono', class: 'font-mono text-white/80',
format: (value: unknown) => (typeof value === 'number' ? value.toFixed(1) : '0.0') format: (value: unknown) => (typeof value === 'number' ? value.toFixed(1) : '0.0')
}, },
{ {
@@ -101,14 +104,14 @@
label: 'Headshots', label: 'Headshots',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono' class: 'font-mono text-white/80'
}, },
{ {
key: 'kills' as const, key: 'kills' as const,
label: 'Kills', label: 'Kills',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono' class: 'font-mono text-white/80'
}, },
{ {
key: 'dmg_team' as const, key: 'dmg_team' as const,
@@ -118,14 +121,13 @@
class: 'font-mono', class: 'font-mono',
render: (value: unknown) => { render: (value: unknown) => {
const dmg = typeof value === 'number' ? value : 0; const dmg = typeof value === 'number' ? value : 0;
if (!dmg || dmg === 0) return '<span class="text-base-content/40">-</span>'; if (!dmg || dmg === 0) return '<span class="text-white/30">-</span>';
return `<span class="text-error">${dmg.toLocaleString()}</span>`; return `<span class="text-neon-red">${dmg.toLocaleString()}</span>`;
} }
} }
]; ];
// Hit group distribution data (placeholder - would need weapon stats data) // Utility damage data with neon colors
// For now, showing utility damage breakdown instead
const utilityDamageData = hasPlayerData const utilityDamageData = hasPlayerData
? { ? {
labels: ['HE Grenades', 'Fire (Molotov/Inc)'], labels: ['HE Grenades', 'Fire (Molotov/Inc)'],
@@ -137,10 +139,10 @@
playersWithDamageStats.reduce((sum, p) => sum + (p.ud_flames || 0), 0) playersWithDamageStats.reduce((sum, p) => sum + (p.ud_flames || 0), 0)
], ],
backgroundColor: [ backgroundColor: [
'rgba(34, 197, 94, 0.8)', // Green for HE 'rgba(0, 255, 136, 0.8)', // neon-green for HE
'rgba(239, 68, 68, 0.8)' // Red for Fire 'rgba(255, 51, 102, 0.8)' // neon-red for Fire
], ],
borderColor: ['rgba(34, 197, 94, 1)', 'rgba(239, 68, 68, 1)'], borderColor: ['#00ff88', '#ff3366'],
borderWidth: 2 borderWidth: 2
} }
] ]
@@ -158,10 +160,13 @@
{#if !hasPlayerData} {#if !hasPlayerData}
<Card padding="lg"> <Card padding="lg">
<div class="text-center"> <div class="text-center">
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" /> <AlertCircle
<h2 class="mb-2 text-2xl font-bold text-base-content">No Player Data Available</h2> class="mx-auto mb-4 h-16 w-16 text-neon-gold"
<p class="mb-4 text-base-content/60"> style="filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.4));"
Detailed damage statistics are not available for this match. />
<h2 class="mb-2 text-2xl font-bold text-white">No Player Data Available</h2>
<p class="mb-4 text-white/60">
Detailed damage statistics are not available for this match. The pain remains unquantified.
</p> </p>
<Badge variant="warning" size="lg">Player data unavailable</Badge> <Badge variant="warning" size="lg">Player data unavailable</Badge>
</div> </div>
@@ -171,18 +176,18 @@
<!-- Team Damage Summary Cards --> <!-- Team Damage Summary Cards -->
<div class="grid gap-6 md:grid-cols-2"> <div class="grid gap-6 md:grid-cols-2">
<!-- Terrorists Damage Stats --> <!-- Terrorists Damage Stats -->
<Card padding="lg"> <Card padding="lg" class="border-l-4 border-l-terrorist">
<h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists Damage</h3> <h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists Damage</h3>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<div class="text-sm text-base-content/60">Total Damage</div> <div class="text-sm text-white/50">Total Damage</div>
<div class="text-3xl font-bold text-base-content"> <div class="text-3xl font-bold text-white">
{teamAStats.totalDamage.toLocaleString()} {teamAStats.totalDamage.toLocaleString()}
</div> </div>
</div> </div>
<div> <div>
<div class="text-sm text-base-content/60">Avg per Player</div> <div class="text-sm text-white/50">Avg per Player</div>
<div class="text-3xl font-bold text-base-content"> <div class="text-3xl font-bold text-white">
{Math.round(teamAStats.avgDamagePerPlayer).toLocaleString()} {Math.round(teamAStats.avgDamagePerPlayer).toLocaleString()}
</div> </div>
</div> </div>
@@ -190,18 +195,18 @@
</Card> </Card>
<!-- Counter-Terrorists Damage Stats --> <!-- Counter-Terrorists Damage Stats -->
<Card padding="lg"> <Card padding="lg" class="border-l-4 border-l-ct">
<h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists Damage</h3> <h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists Damage</h3>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<div class="text-sm text-base-content/60">Total Damage</div> <div class="text-sm text-white/50">Total Damage</div>
<div class="text-3xl font-bold text-base-content"> <div class="text-3xl font-bold text-white">
{teamBStats.totalDamage.toLocaleString()} {teamBStats.totalDamage.toLocaleString()}
</div> </div>
</div> </div>
<div> <div>
<div class="text-sm text-base-content/60">Avg per Player</div> <div class="text-sm text-white/50">Avg per Player</div>
<div class="text-3xl font-bold text-base-content"> <div class="text-3xl font-bold text-white">
{Math.round(teamBStats.avgDamagePerPlayer).toLocaleString()} {Math.round(teamBStats.avgDamagePerPlayer).toLocaleString()}
</div> </div>
</div> </div>
@@ -215,47 +220,97 @@
<Card padding="lg"> <Card padding="lg">
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Target <div
class="h-5 w-5 {index === 0 class="flex h-8 w-8 items-center justify-center rounded-lg {index === 0
? 'text-warning' ? 'bg-neon-gold/20'
: index === 1 : index === 1
? 'text-base-content/70' ? 'bg-white/10'
: 'text-base-content/50'}" : 'bg-white/5'}"
/> style={index === 0 ? 'box-shadow: 0 0 10px rgba(255, 215, 0, 0.2);' : ''}
<h3 class="font-semibold text-base-content"> >
<Target
class="h-4 w-4 {index === 0
? 'text-neon-gold'
: index === 1
? 'text-white/70'
: 'text-white/50'}"
/>
</div>
<h3 class="font-semibold text-white">
#{index + 1} Damage Dealer #{index + 1} Damage Dealer
</h3> </h3>
</div> </div>
</div> </div>
<div <a
class="text-2xl font-bold {player.team_id === firstTeamId href={`/player/${player.id}`}
class="text-2xl font-bold transition-colors hover:text-neon-blue {player.team_id ===
firstTeamId
? 'text-terrorist' ? 'text-terrorist'
: 'text-ct'}" : 'text-ct'}"
> >
{player.name} {player.name}
</div> </a>
<div class="mt-1 font-mono text-3xl font-bold text-primary"> <div
class="mt-1 font-mono text-3xl font-bold text-neon-blue"
style="text-shadow: 0 0 15px rgba(0, 212, 255, 0.4);"
>
{player.damage.toLocaleString()} {player.damage.toLocaleString()}
</div> </div>
<div class="mt-2 text-xs text-base-content/60"> <div class="mt-2 text-xs text-white/50">
{player.avgDamagePerRound.toFixed(1)} ADR {player.avgDamagePerRound.toFixed(1)} ADR
</div> </div>
</Card> </Card>
{/each} {/each}
</div> </div>
<!-- Needs Therapy Badge -->
{#if needsTherapyPlayer && (needsTherapyPlayer.dmg_team || 0) > 50}
<Card padding="lg" class="border-neon-red/30 bg-neon-red/5">
<div class="flex items-center gap-4">
<div
class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-red/20"
style="box-shadow: 0 0 15px rgba(255, 51, 102, 0.3);"
>
<Skull class="h-6 w-6 text-neon-red" />
</div>
<div>
<h3 class="text-lg font-bold text-neon-red">Needs Therapy Award</h3>
<p class="text-sm text-white/60">
<a
href={`/player/${needsTherapyPlayer.id}`}
class="font-medium text-white hover:text-neon-blue"
>
{needsTherapyPlayer.name}
</a>
dealt
<span class="font-mono font-bold text-neon-red">{needsTherapyPlayer.dmg_team}</span> damage
to their own team. Apologize in chat!
</p>
</div>
</div>
</Card>
{/if}
<!-- Utility Damage Distribution --> <!-- Utility Damage Distribution -->
<Card padding="lg"> <Card padding="lg">
<div class="mb-4"> <div class="mb-4 flex items-center gap-3">
<h2 class="text-2xl font-bold text-base-content">Utility Damage Distribution</h2> <div
<p class="text-sm text-base-content/60"> class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-red/20"
Breakdown of damage dealt by grenades and fire across all players style="box-shadow: 0 0 15px rgba(255, 51, 102, 0.2);"
</p> >
<Flame class="h-5 w-5 text-neon-red" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Utility Damage Distribution</h2>
<p class="text-sm text-white/50">
Breakdown of damage dealt by grenades and fire - The Molotov Mixologist's Report
</p>
</div>
</div> </div>
{#if utilityDamageData.datasets.length > 0 && utilityDamageData.datasets[0]?.data.some((v) => v > 0)} {#if utilityDamageData.datasets.length > 0 && utilityDamageData.datasets[0]?.data.some((v) => v > 0)}
<PieChart data={utilityDamageData} height={300} /> <PieChart data={utilityDamageData} height={300} />
{:else} {:else}
<div class="py-12 text-center text-base-content/40"> <div class="py-12 text-center text-white/40">
<Crosshair class="mx-auto mb-2 h-12 w-12" /> <Crosshair class="mx-auto mb-2 h-12 w-12" />
<p>No utility damage recorded for this match</p> <p>No utility damage recorded for this match</p>
</div> </div>
@@ -265,20 +320,27 @@
<!-- Player Damage Table --> <!-- Player Damage Table -->
<Card padding="none"> <Card padding="none">
<div class="p-6"> <div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Player Damage Statistics</h2> <h2 class="text-2xl font-bold text-white">Player Damage Statistics</h2>
<p class="mt-1 text-sm text-base-content/60">Detailed damage breakdown for all players</p> <p class="mt-1 text-sm text-white/50">
Detailed damage breakdown for all players - The pain ledger
</p>
</div> </div>
<DataTable data={sortedByDamage} columns={damageColumns} striped hoverable /> <DataTable data={sortedByDamage} columns={damageColumns} striped hoverable />
</Card> </Card>
<!-- Additional Info Note --> <!-- Additional Info Note -->
<Card padding="lg"> <Card padding="lg" class="border-neon-blue/20">
<div class="flex items-start gap-3"> <div class="flex items-start gap-4">
<AlertCircle class="h-5 w-5 flex-shrink-0 text-info" /> <div
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-neon-blue/20"
style="box-shadow: 0 0 15px rgba(0, 212, 255, 0.2);"
>
<Lightbulb class="h-5 w-5 text-neon-blue" />
</div>
<div class="text-sm"> <div class="text-sm">
<h3 class="mb-1 font-semibold text-base-content">About Damage Statistics</h3> <h3 class="mb-1 font-semibold text-white">About Damage Statistics</h3>
<p class="text-base-content/70"> <p class="text-white/60">
Damage statistics show total damage dealt to enemies throughout the match. Average Damage statistics show total damage dealt to enemies throughout the match. Average
damage per round (ADR) is calculated by dividing total damage by the number of rounds damage per round (ADR) is calculated by dividing total damage by the number of rounds
played. Hit group breakdown (head, chest, legs, etc.) is available in weapon-specific played. Hit group breakdown (head, chest, legs, etc.) is available in weapon-specific

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Trophy, Target, Flame, AlertCircle } from 'lucide-svelte'; import { Trophy, Target, Flame, AlertCircle, Crosshair } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte'; import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte'; import Badge from '$lib/components/ui/Badge.svelte';
import DataTable from '$lib/components/data-display/DataTable.svelte'; import DataTable from '$lib/components/data-display/DataTable.svelte';
@@ -62,7 +62,7 @@
align: 'center' as const, align: 'center' as const,
render: (_value: unknown, row: PlayerWithStats) => { render: (_value: unknown, row: PlayerWithStats) => {
const avatarUrl = row.avatar || ''; const avatarUrl = row.avatar || '';
return `<img src="${avatarUrl}" alt="${row.name}" class="h-10 w-10 rounded-full border-2 border-base-300" />`; return `<img src="${avatarUrl}" alt="${row.name}" class="h-10 w-10 rounded-full border-2 border-white/10" />`;
} }
}, },
{ {
@@ -77,7 +77,7 @@
const colorDot = colorHex const colorDot = colorHex
? `<span class="inline-block h-3 w-3 rounded-full mr-2" style="background-color: ${colorHex}"></span>` ? `<span class="inline-block h-3 w-3 rounded-full mr-2" style="background-color: ${colorHex}"></span>`
: ''; : '';
return `<a href="/player/${row.id}" class="flex items-center font-medium hover:underline ${teamClass}">${colorDot}${strValue}</a>`; return `<a href="/player/${row.id}" class="flex items-center font-medium hover:text-neon-blue transition-colors ${teamClass}">${colorDot}${strValue}</a>`;
} }
}, },
{ {
@@ -85,35 +85,35 @@
label: 'Score', label: 'Score',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono font-semibold' class: 'font-mono font-semibold text-white'
}, },
{ {
key: 'kills' as keyof (typeof playersWithStats)[0], key: 'kills' as keyof (typeof playersWithStats)[0],
label: 'K', label: 'K',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono font-semibold' class: 'font-mono font-semibold text-white'
}, },
{ {
key: 'deaths' as keyof (typeof playersWithStats)[0], key: 'deaths' as keyof (typeof playersWithStats)[0],
label: 'D', label: 'D',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono' class: 'font-mono text-white/80'
}, },
{ {
key: 'assists' as keyof (typeof playersWithStats)[0], key: 'assists' as keyof (typeof playersWithStats)[0],
label: 'A', label: 'A',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono' class: 'font-mono text-white/80'
}, },
{ {
key: 'kd' as keyof PlayerWithStats, key: 'kd' as keyof PlayerWithStats,
label: 'K/D', label: 'K/D',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono', class: 'font-mono text-white/80',
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(2) : '0.00') format: (v: unknown) => (v !== undefined ? (v as number).toFixed(2) : '0.00')
}, },
{ {
@@ -121,7 +121,7 @@
label: 'ADR', label: 'ADR',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono', class: 'font-mono text-white/80',
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '0.0') format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '0.0')
}, },
{ {
@@ -129,7 +129,7 @@
label: 'HS%', label: 'HS%',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono', class: 'font-mono text-white/80',
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '0.0') format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '0.0')
}, },
{ {
@@ -137,7 +137,7 @@
label: 'KAST%', label: 'KAST%',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono', class: 'font-mono text-white/80',
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '-') format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '-')
}, },
{ {
@@ -145,7 +145,7 @@
label: 'MVP', label: 'MVP',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono' class: 'font-mono text-white/80'
}, },
{ {
key: 'mk_5' as keyof (typeof playersWithStats)[0], key: 'mk_5' as keyof (typeof playersWithStats)[0],
@@ -157,8 +157,9 @@
_row: (typeof playersWithStats)[0] _row: (typeof playersWithStats)[0]
) => { ) => {
const numValue = value !== undefined ? (value as number) : 0; const numValue = value !== undefined ? (value as number) : 0;
if (numValue > 0) return `<span class="badge badge-warning badge-sm">${numValue}</span>`; if (numValue > 0)
return '<span class="text-base-content/40">-</span>'; return `<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-bold bg-neon-gold/20 text-neon-gold border border-neon-gold/30">${numValue}</span>`;
return '<span class="text-white/30">-</span>';
} }
}, },
{ {
@@ -172,42 +173,54 @@
) => { ) => {
const badges = []; const badges = [];
if (row.vac) { if (row.vac) {
badges.push('<span class="badge badge-error badge-sm" title="VAC Banned">VAC</span>'); badges.push(
'<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-bold bg-neon-red/20 text-neon-red border border-neon-red/30" title="VAC Banned">VAC</span>'
);
} }
if (row.game_ban) { if (row.game_ban) {
badges.push('<span class="badge badge-error badge-sm" title="Game Banned">BAN</span>'); badges.push(
'<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-bold bg-neon-red/20 text-neon-red border border-neon-red/30" title="Game Banned">BAN</span>'
);
} }
if (badges.length > 0) { if (badges.length > 0) {
return `<div class="flex gap-1 justify-center">${badges.join('')}</div>`; return `<div class="flex gap-1 justify-center">${badges.join('')}</div>`;
} }
return '<span class="text-base-content/40">-</span>'; return '<span class="text-white/30">-</span>';
} }
} }
]; ];
// Multi-kill chart data // Multi-kill chart data with neon colors
const multiKillData = { const multiKillData = {
labels: sortedPlayers.map((p) => p.name), labels: sortedPlayers.map((p) => p.name),
datasets: [ datasets: [
{ {
label: '2K', label: '2K',
data: sortedPlayers.map((p) => p.mk_2 || 0), data: sortedPlayers.map((p) => p.mk_2 || 0),
backgroundColor: 'rgba(34, 197, 94, 0.8)' backgroundColor: 'rgba(0, 255, 136, 0.7)', // neon-green
borderColor: '#00ff88',
borderWidth: 1
}, },
{ {
label: '3K', label: '3K',
data: sortedPlayers.map((p) => p.mk_3 || 0), data: sortedPlayers.map((p) => p.mk_3 || 0),
backgroundColor: 'rgba(59, 130, 246, 0.8)' backgroundColor: 'rgba(0, 212, 255, 0.7)', // neon-blue
borderColor: '#00d4ff',
borderWidth: 1
}, },
{ {
label: '4K', label: '4K',
data: sortedPlayers.map((p) => p.mk_4 || 0), data: sortedPlayers.map((p) => p.mk_4 || 0),
backgroundColor: 'rgba(249, 115, 22, 0.8)' backgroundColor: 'rgba(255, 215, 0, 0.7)', // neon-gold
borderColor: '#ffd700',
borderWidth: 1
}, },
{ {
label: '5K (Ace)', label: '5K (Ace)',
data: sortedPlayers.map((p) => p.mk_5 || 0), data: sortedPlayers.map((p) => p.mk_5 || 0),
backgroundColor: 'rgba(239, 68, 68, 0.8)' backgroundColor: 'rgba(255, 51, 102, 0.7)', // neon-red
borderColor: '#ff3366',
borderWidth: 1
} }
] ]
}; };
@@ -264,10 +277,14 @@
{#if !hasPlayerData} {#if !hasPlayerData}
<Card padding="lg"> <Card padding="lg">
<div class="text-center"> <div class="text-center">
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" /> <AlertCircle
<h2 class="mb-2 text-2xl font-bold text-base-content">No Player Data Available</h2> class="mx-auto mb-4 h-16 w-16 text-neon-gold"
<p class="mb-4 text-base-content/60"> style="filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.4));"
Detailed player statistics are not available for this match. />
<h2 class="mb-2 text-2xl font-bold text-white">No Player Data Available</h2>
<p class="mb-4 text-white/60">
Detailed player statistics are not available for this match. The scoreboard mysteries remain
unsolved.
</p> </p>
<Badge variant="warning" size="lg">Player data unavailable</Badge> <Badge variant="warning" size="lg">Player data unavailable</Badge>
</div> </div>
@@ -277,47 +294,55 @@
<!-- Team Performance Summary --> <!-- Team Performance Summary -->
<div class="grid gap-6 md:grid-cols-2"> <div class="grid gap-6 md:grid-cols-2">
<!-- Terrorists Stats --> <!-- Terrorists Stats -->
<Card padding="lg"> <Card padding="lg" class="border-l-4 border-l-terrorist">
<h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists Performance</h3> <h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists Performance</h3>
<div class="grid grid-cols-2 gap-4 text-sm"> <div class="grid grid-cols-2 gap-4 text-sm">
<div> <div>
<div class="text-base-content/60">Total Damage</div> <div class="text-white/50">Total Damage</div>
<div class="text-2xl font-bold">{teamAStats.totalDamage.toLocaleString()}</div> <div class="text-2xl font-bold text-white">
{teamAStats.totalDamage.toLocaleString()}
</div>
</div> </div>
<div> <div>
<div class="text-base-content/60">Utility Damage</div> <div class="text-white/50">Utility Damage</div>
<div class="text-2xl font-bold">{teamAStats.totalUtilityDamage.toLocaleString()}</div> <div class="text-2xl font-bold text-white">
{teamAStats.totalUtilityDamage.toLocaleString()}
</div>
</div> </div>
<div> <div>
<div class="text-base-content/60">Flash Assists</div> <div class="text-white/50">Flash Assists</div>
<div class="text-2xl font-bold">{teamAStats.totalFlashAssists}</div> <div class="text-2xl font-bold text-white">{teamAStats.totalFlashAssists}</div>
</div> </div>
<div> <div>
<div class="text-base-content/60">Avg KAST</div> <div class="text-white/50">Avg KAST</div>
<div class="text-2xl font-bold">{teamAStats.avgKAST}%</div> <div class="text-2xl font-bold text-white">{teamAStats.avgKAST}%</div>
</div> </div>
</div> </div>
</Card> </Card>
<!-- Counter-Terrorists Stats --> <!-- Counter-Terrorists Stats -->
<Card padding="lg"> <Card padding="lg" class="border-l-4 border-l-ct">
<h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists Performance</h3> <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 class="grid grid-cols-2 gap-4 text-sm">
<div> <div>
<div class="text-base-content/60">Total Damage</div> <div class="text-white/50">Total Damage</div>
<div class="text-2xl font-bold">{teamBStats.totalDamage.toLocaleString()}</div> <div class="text-2xl font-bold text-white">
{teamBStats.totalDamage.toLocaleString()}
</div>
</div> </div>
<div> <div>
<div class="text-base-content/60">Utility Damage</div> <div class="text-white/50">Utility Damage</div>
<div class="text-2xl font-bold">{teamBStats.totalUtilityDamage.toLocaleString()}</div> <div class="text-2xl font-bold text-white">
{teamBStats.totalUtilityDamage.toLocaleString()}
</div>
</div> </div>
<div> <div>
<div class="text-base-content/60">Flash Assists</div> <div class="text-white/50">Flash Assists</div>
<div class="text-2xl font-bold">{teamBStats.totalFlashAssists}</div> <div class="text-2xl font-bold text-white">{teamBStats.totalFlashAssists}</div>
</div> </div>
<div> <div>
<div class="text-base-content/60">Avg KAST</div> <div class="text-white/50">Avg KAST</div>
<div class="text-2xl font-bold">{teamBStats.avgKAST}%</div> <div class="text-2xl font-bold text-white">{teamBStats.avgKAST}%</div>
</div> </div>
</div> </div>
</Card> </Card>
@@ -325,21 +350,59 @@
<!-- Multi-Kills Chart --> <!-- Multi-Kills Chart -->
<Card padding="lg"> <Card padding="lg">
<div class="mb-4"> <div class="mb-4 flex items-center gap-3">
<h2 class="text-2xl font-bold text-base-content">Multi-Kill Distribution</h2> <div
<p class="text-sm text-base-content/60"> class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-red/20"
Double kills (2K), triple kills (3K), quad kills (4K), and aces (5K) per player style="box-shadow: 0 0 15px rgba(255, 51, 102, 0.2);"
</p> >
<Crosshair class="h-5 w-5 text-neon-red" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Multi-Threat Level</h2>
<p class="text-sm text-white/50">
Double kills, triple kills, quad kills, and aces - Who went absolutely mental
</p>
</div>
</div> </div>
<BarChart data={multiKillData} height={300} /> <BarChart
data={multiKillData}
height={300}
options={{
scales: {
y: {
grid: {
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: 'rgba(255, 255, 255, 0.5)'
}
},
x: {
grid: {
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: 'rgba(255, 255, 255, 0.5)'
}
}
},
plugins: {
legend: {
labels: {
color: 'rgba(255, 255, 255, 0.7)'
}
}
}
}}
/>
</Card> </Card>
<!-- Detailed Player Statistics Table --> <!-- Detailed Player Statistics Table -->
<Card padding="none"> <Card padding="none">
<div class="p-6"> <div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Detailed Player Statistics</h2> <h2 class="text-2xl font-bold text-white">Detailed Player Statistics</h2>
<p class="mt-1 text-sm text-base-content/60"> <p class="mt-1 text-sm text-white/50">
Complete performance breakdown for all players Complete performance breakdown for all players - The full criminal record
</p> </p>
</div> </div>
@@ -352,14 +415,27 @@
<!-- Most Kills --> <!-- Most Kills -->
<Card padding="lg"> <Card padding="lg">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<Trophy class="h-5 w-5 text-warning" /> <div
<h3 class="font-semibold text-base-content">Most Kills</h3> class="flex h-8 w-8 items-center justify-center rounded-lg bg-neon-gold/20"
style="box-shadow: 0 0 10px rgba(255, 215, 0, 0.2);"
>
<Trophy class="h-4 w-4 text-neon-gold" />
</div>
<h3 class="font-semibold text-white">Most Kills</h3>
</div> </div>
<div class="text-2xl font-bold text-base-content">{sortedPlayers[0].name}</div> <a
<div class="mt-1 font-mono text-3xl font-bold text-primary"> href={`/player/${sortedPlayers[0].id}`}
class="text-2xl font-bold text-white transition-colors hover:text-neon-blue"
>
{sortedPlayers[0].name}
</a>
<div
class="mt-1 font-mono text-3xl font-bold text-neon-blue"
style="text-shadow: 0 0 15px rgba(0, 212, 255, 0.4);"
>
{sortedPlayers[0].kills} {sortedPlayers[0].kills}
</div> </div>
<div class="mt-2 text-xs text-base-content/60"> <div class="mt-2 text-xs text-white/50">
{sortedPlayers[0].deaths} deaths, {sortedPlayers[0].kd.toFixed(2)} K/D {sortedPlayers[0].deaths} deaths, {sortedPlayers[0].kd.toFixed(2)} K/D
</div> </div>
</Card> </Card>
@@ -369,12 +445,27 @@
{#if bestKD} {#if bestKD}
<Card padding="lg"> <Card padding="lg">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<Target class="h-5 w-5 text-success" /> <div
<h3 class="font-semibold text-base-content">Best K/D Ratio</h3> class="flex h-8 w-8 items-center justify-center rounded-lg bg-neon-green/20"
style="box-shadow: 0 0 10px rgba(0, 255, 136, 0.2);"
>
<Target class="h-4 w-4 text-neon-green" />
</div>
<h3 class="font-semibold text-white">Can't Touch This</h3>
</div> </div>
<div class="text-2xl font-bold text-base-content">{bestKD.name}</div> <a
<div class="mt-1 font-mono text-3xl font-bold text-success">{bestKD.kd.toFixed(2)}</div> href={`/player/${bestKD.id}`}
<div class="mt-2 text-xs text-base-content/60"> class="text-2xl font-bold text-white transition-colors hover:text-neon-blue"
>
{bestKD.name}
</a>
<div
class="mt-1 font-mono text-3xl font-bold text-neon-green"
style="text-shadow: 0 0 15px rgba(0, 255, 136, 0.4);"
>
{bestKD.kd.toFixed(2)}
</div>
<div class="mt-2 text-xs text-white/50">
{bestKD.kills}K / {bestKD.deaths}D {bestKD.kills}K / {bestKD.deaths}D
</div> </div>
</Card> </Card>
@@ -387,14 +478,27 @@
{#if bestUtility} {#if bestUtility}
<Card padding="lg"> <Card padding="lg">
<div class="mb-3 flex items-center gap-2"> <div class="mb-3 flex items-center gap-2">
<Flame class="h-5 w-5 text-error" /> <div
<h3 class="font-semibold text-base-content">Most Utility Damage</h3> class="flex h-8 w-8 items-center justify-center rounded-lg bg-neon-red/20"
style="box-shadow: 0 0 10px rgba(255, 51, 102, 0.2);"
>
<Flame class="h-4 w-4 text-neon-red" />
</div>
<h3 class="font-semibold text-white">The Molotov Mixologist</h3>
</div> </div>
<div class="text-2xl font-bold text-base-content">{bestUtility.name}</div> <a
<div class="mt-1 font-mono text-3xl font-bold text-error"> href={`/player/${bestUtility.id}`}
class="text-2xl font-bold text-white transition-colors hover:text-neon-blue"
>
{bestUtility.name}
</a>
<div
class="mt-1 font-mono text-3xl font-bold text-neon-red"
style="text-shadow: 0 0 15px rgba(255, 51, 102, 0.4);"
>
{((bestUtility.ud_he || 0) + (bestUtility.ud_flames || 0)).toLocaleString()} {((bestUtility.ud_he || 0) + (bestUtility.ud_flames || 0)).toLocaleString()}
</div> </div>
<div class="mt-2 text-xs text-base-content/60"> <div class="mt-2 text-xs text-white/50">
HE: {bestUtility.ud_he || 0} | Fire: {bestUtility.ud_flames || 0} HE: {bestUtility.ud_he || 0} | Fire: {bestUtility.ud_flames || 0}
</div> </div>
</Card> </Card>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { TrendingUp, ShoppingCart, AlertCircle } from 'lucide-svelte'; import { TrendingUp, ShoppingCart, AlertCircle, Wallet, DollarSign } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte'; import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte'; import Badge from '$lib/components/ui/Badge.svelte';
import LineChart from '$lib/components/charts/LineChart.svelte'; import LineChart from '$lib/components/charts/LineChart.svelte';
@@ -17,7 +17,7 @@
winner: number; winner: number;
teamA_buyType: string; teamA_buyType: string;
teamB_buyType: string; teamB_buyType: string;
economyAdvantage: number; // Cumulative economy differential (teamA - teamB) economyAdvantage: number;
} }
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@@ -28,8 +28,6 @@
const ctTeamId = 3; const ctTeamId = 3;
// Calculate halftime round based on max_rounds // Calculate halftime round based on max_rounds
// MR12 (24 rounds): halftime after round 12
// MR15 (30 rounds): halftime after round 15
const halfPoint = match.max_rounds === 30 ? 15 : 12; const halfPoint = match.max_rounds === 30 ? 15 : 12;
// Only process if rounds data exists // Only process if rounds data exists
@@ -93,19 +91,13 @@
return 'Full Buy'; return 'Full Buy';
}; };
// Calculate per-round economy advantage using bank + spent (like old portal)
// Teams swap sides at halftime, so we need to account for perspective flip
const t_totalEconomy = t_bank + t_spent; const t_totalEconomy = t_bank + t_spent;
const ct_totalEconomy = ct_bank + ct_spent; const ct_totalEconomy = ct_bank + ct_spent;
// Determine perspective based on round (teams swap at half)
// halfPoint is calculated above based on match.max_rounds
let economyAdvantage; let economyAdvantage;
if (roundData.round <= halfPoint) { if (roundData.round <= halfPoint) {
// First half: T - CT
economyAdvantage = t_totalEconomy - ct_totalEconomy; economyAdvantage = t_totalEconomy - ct_totalEconomy;
} else { } else {
// Second half: CT - T (teams swapped sides)
economyAdvantage = ct_totalEconomy - t_totalEconomy; economyAdvantage = ct_totalEconomy - t_totalEconomy;
} }
@@ -124,23 +116,23 @@
}); });
} }
// Prepare equipment value chart data // Prepare equipment value chart data with neon colors
equipmentChartData = { equipmentChartData = {
labels: teamEconomy.map((r) => `R${r.round}`), labels: teamEconomy.map((r) => `R${r.round}`),
datasets: [ datasets: [
{ {
label: 'Terrorists Equipment', label: 'Terrorists Equipment',
data: teamEconomy.map((r) => r.teamA_equipment), data: teamEconomy.map((r) => r.teamA_equipment),
borderColor: 'rgb(249, 115, 22)', borderColor: '#d4a74a', // terrorist color
backgroundColor: 'rgba(249, 115, 22, 0.1)', backgroundColor: 'rgba(212, 167, 74, 0.1)',
fill: true, fill: true,
tension: 0.4 tension: 0.4
}, },
{ {
label: 'Counter-Terrorists Equipment', label: 'Counter-Terrorists Equipment',
data: teamEconomy.map((r) => r.teamB_equipment), data: teamEconomy.map((r) => r.teamB_equipment),
borderColor: 'rgb(59, 130, 246)', borderColor: '#5e98d9', // ct color
backgroundColor: 'rgba(59, 130, 246, 0.1)', backgroundColor: 'rgba(94, 152, 217, 0.1)',
fill: true, fill: true,
tension: 0.4 tension: 0.4
} }
@@ -148,7 +140,6 @@
}; };
// Prepare economy advantage chart data // Prepare economy advantage chart data
// Positive = above 0, Negative = below 0
halfRoundIndex = Math.floor(teamEconomy.length / 2); halfRoundIndex = Math.floor(teamEconomy.length / 2);
economyAdvantageChartData = { economyAdvantageChartData = {
labels: teamEconomy.map((r) => `${r.round}`), labels: teamEconomy.map((r) => `${r.round}`),
@@ -156,8 +147,8 @@
{ {
label: 'Advantage', label: 'Advantage',
data: teamEconomy.map((r) => (r.economyAdvantage > 0 ? r.economyAdvantage : 0)), data: teamEconomy.map((r) => (r.economyAdvantage > 0 ? r.economyAdvantage : 0)),
borderColor: 'rgb(59, 130, 246)', borderColor: '#5e98d9',
backgroundColor: 'rgba(59, 130, 246, 0.6)', backgroundColor: 'rgba(94, 152, 217, 0.6)',
// @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid // @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid
fill: 'origin', fill: 'origin',
tension: 0.4, tension: 0.4,
@@ -167,8 +158,8 @@
{ {
label: 'Disadvantage', label: 'Disadvantage',
data: teamEconomy.map((r) => (r.economyAdvantage < 0 ? r.economyAdvantage : 0)), data: teamEconomy.map((r) => (r.economyAdvantage < 0 ? r.economyAdvantage : 0)),
borderColor: 'rgb(249, 115, 22)', borderColor: '#d4a74a',
backgroundColor: 'rgba(249, 115, 22, 0.6)', backgroundColor: 'rgba(212, 167, 74, 0.6)',
// @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid // @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid
fill: 'origin', fill: 'origin',
tension: 0.4, tension: 0.4,
@@ -186,6 +177,14 @@
teamB_ecos = teamEconomy.filter((r) => r.teamB_buyType === 'Eco').length; teamB_ecos = teamEconomy.filter((r) => r.teamB_buyType === 'Eco').length;
} }
// Buy type labels with puns
const buyTypeLabels: Record<string, string> = {
Eco: 'The Poverty Round',
'Semi-Eco': 'Broke but Hopeful',
Force: 'YOLO Buy',
'Full Buy': 'Loaded'
};
// Table columns // Table columns
const tableColumns = [ const tableColumns = [
{ {
@@ -200,15 +199,15 @@
sortable: true, sortable: true,
render: (value: string | number | boolean, _row: TeamEconomy) => { render: (value: string | number | boolean, _row: TeamEconomy) => {
const strValue = value as string; const strValue = value as string;
const variant = const colorClass =
strValue === 'Full Buy' strValue === 'Full Buy'
? 'success' ? 'bg-neon-green/20 text-neon-green border-neon-green/30'
: strValue === 'Eco' : strValue === 'Eco'
? 'error' ? 'bg-neon-red/20 text-neon-red border-neon-red/30'
: strValue === 'Force' : strValue === 'Force'
? 'warning' ? 'bg-neon-gold/20 text-neon-gold border-neon-gold/30'
: 'default'; : 'bg-white/10 text-white/60 border-white/20';
return `<span class="badge badge-${variant} badge-sm">${strValue}</span>`; return `<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border ${colorClass}">${strValue}</span>`;
} }
}, },
{ {
@@ -225,15 +224,15 @@
sortable: true, sortable: true,
render: (value: string | number | boolean, _row: TeamEconomy) => { render: (value: string | number | boolean, _row: TeamEconomy) => {
const strValue = value as string; const strValue = value as string;
const variant = const colorClass =
strValue === 'Full Buy' strValue === 'Full Buy'
? 'success' ? 'bg-neon-green/20 text-neon-green border-neon-green/30'
: strValue === 'Eco' : strValue === 'Eco'
? 'error' ? 'bg-neon-red/20 text-neon-red border-neon-red/30'
: strValue === 'Force' : strValue === 'Force'
? 'warning' ? 'bg-neon-gold/20 text-neon-gold border-neon-gold/30'
: 'default'; : 'bg-white/10 text-white/60 border-white/20';
return `<span class="badge badge-${variant} badge-sm">${strValue}</span>`; return `<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border ${colorClass}">${strValue}</span>`;
} }
}, },
{ {
@@ -251,10 +250,10 @@
render: (value: string | number | boolean, _row: TeamEconomy) => { render: (value: string | number | boolean, _row: TeamEconomy) => {
const numValue = value as number; const numValue = value as number;
if (numValue === 2) if (numValue === 2)
return '<span class="badge badge-sm" style="background-color: rgb(249, 115, 22); color: white;">T</span>'; return '<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-bold bg-terrorist/20 text-terrorist border border-terrorist/30">T</span>';
if (numValue === 3) if (numValue === 3)
return '<span class="badge badge-sm" style="background-color: rgb(59, 130, 246); color: white;">CT</span>'; return '<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-bold bg-ct/20 text-ct border border-ct/30">CT</span>';
return '<span class="text-base-content/40">-</span>'; return '<span class="text-white/30">-</span>';
} }
} }
]; ];
@@ -263,10 +262,14 @@
{#if !roundsData} {#if !roundsData}
<Card padding="lg"> <Card padding="lg">
<div class="text-center"> <div class="text-center">
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" /> <AlertCircle
<h2 class="mb-2 text-2xl font-bold text-base-content">Match Not Parsed</h2> class="mx-auto mb-4 h-16 w-16 text-neon-gold"
<p class="mb-4 text-base-content/60"> style="filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.4));"
This match hasn't been parsed yet, so detailed economy data is not available. />
<h2 class="mb-2 text-2xl font-bold text-white">Match Not Parsed</h2>
<p class="mb-4 text-white/60">
This match hasn't been parsed yet, so detailed economy data is not available. The evidence
of everyone's financial decisions remains hidden.
</p> </p>
<Badge variant="warning" size="lg">Demo parsing required</Badge> <Badge variant="warning" size="lg">Demo parsing required</Badge>
</div> </div>
@@ -275,9 +278,19 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Economy Advantage Chart --> <!-- Economy Advantage Chart -->
<Card padding="lg"> <Card padding="lg">
<div class="mb-4"> <div class="mb-4 flex items-center gap-3">
<h2 class="text-2xl font-bold text-base-content">Economy</h2> <div
<p class="text-sm text-base-content/60">Net-worth differential (bank + spent)</p> 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);"
>
<DollarSign class="h-5 w-5 text-neon-green" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Economy Flow</h2>
<p class="text-sm text-white/50">
Net-worth differential (bank + spent) - The money story
</p>
</div>
</div> </div>
{#if economyAdvantageChartData} {#if economyAdvantageChartData}
<div class="relative"> <div class="relative">
@@ -291,19 +304,37 @@
grid: { grid: {
color: (context) => { color: (context) => {
if (context.tick.value === 0) { if (context.tick.value === 0) {
return 'rgba(156, 163, 175, 0.5)'; // Stronger line at 0 return 'rgba(255, 255, 255, 0.3)';
} }
return 'rgba(156, 163, 175, 0.1)'; return 'rgba(255, 255, 255, 0.05)';
}, },
lineWidth: (context) => { lineWidth: (context) => {
return context.tick.value === 0 ? 2 : 1; return context.tick.value === 0 ? 2 : 1;
} }
},
ticks: {
color: 'rgba(255, 255, 255, 0.5)'
}
},
x: {
grid: {
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: 'rgba(255, 255, 255, 0.5)'
} }
} }
}, },
interaction: { interaction: {
mode: 'index', mode: 'index',
intersect: false intersect: false
},
plugins: {
legend: {
labels: {
color: 'rgba(255, 255, 255, 0.7)'
}
}
} }
}} }}
/> />
@@ -312,11 +343,11 @@
class="pointer-events-none absolute top-0 flex h-full items-center" class="pointer-events-none absolute top-0 flex h-full items-center"
style="left: {(halfRoundIndex / (teamEconomy.length || 1)) * 100}%" style="left: {(halfRoundIndex / (teamEconomy.length || 1)) * 100}%"
> >
<div class="h-full w-px bg-base-content/20"></div> <div class="h-full w-px bg-neon-blue/30"></div>
<div <div
class="absolute -top-1 left-1/2 -translate-x-1/2 rounded bg-base-300 px-2 py-1 text-xs font-medium text-base-content/70" class="absolute -top-1 left-1/2 -translate-x-1/2 rounded-md border border-neon-blue/30 bg-void-light px-2 py-1 text-xs font-medium text-neon-blue"
> >
Half-Point Half-Time
</div> </div>
</div> </div>
{/if} {/if}
@@ -327,54 +358,111 @@
<!-- Summary Cards --> <!-- Summary Cards -->
<div class="grid gap-6 md:grid-cols-3"> <div class="grid gap-6 md:grid-cols-3">
<Card padding="lg"> <Card padding="lg">
<div class="mb-2 flex items-center gap-2"> <div class="flex items-center gap-3">
<ShoppingCart class="h-5 w-5 text-primary" /> <div
<span class="text-sm font-medium text-base-content/70">Total Rounds</span> 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);"
>
<ShoppingCart class="h-5 w-5 text-neon-blue" />
</div>
<div>
<div class="text-sm text-white/50">Total Rounds</div>
<div class="text-3xl font-bold text-white">{totalRounds}</div>
</div>
</div> </div>
<div class="text-3xl font-bold text-base-content">{totalRounds}</div> <div class="mt-2 text-xs text-white/40">
<div class="mt-1 text-xs text-base-content/60">
{match.score_team_a} - {match.score_team_b} {match.score_team_a} - {match.score_team_b}
</div> </div>
</Card> </Card>
<Card padding="lg"> <Card padding="lg" class="border-l-4 border-l-terrorist">
<div class="mb-2 flex items-center gap-2"> <div class="flex items-center gap-3">
<TrendingUp class="h-5 w-5 text-terrorist" /> <div
<span class="text-sm font-medium text-base-content/70">Terrorists Buy Rounds</span> class="flex h-10 w-10 items-center justify-center rounded-lg bg-terrorist/20"
style="box-shadow: 0 0 15px rgba(212, 167, 74, 0.2);"
>
<TrendingUp class="h-5 w-5 text-terrorist" />
</div>
<div>
<div class="text-sm text-white/50">T Full Buys</div>
<div class="text-3xl font-bold text-white">{teamA_fullBuys}</div>
</div>
</div> </div>
<div class="text-3xl font-bold text-base-content">{teamA_fullBuys}</div> <div class="mt-2 text-xs text-neon-red">{teamA_ecos} poverty rounds</div>
<div class="mt-1 text-xs text-base-content/60">{teamA_ecos} eco rounds</div>
</Card> </Card>
<Card padding="lg"> <Card padding="lg" class="border-l-4 border-l-ct">
<div class="mb-2 flex items-center gap-2"> <div class="flex items-center gap-3">
<TrendingUp class="h-5 w-5 text-ct" /> <div
<span class="text-sm font-medium text-base-content/70">CT Buy Rounds</span> class="flex h-10 w-10 items-center justify-center rounded-lg bg-ct/20"
style="box-shadow: 0 0 15px rgba(94, 152, 217, 0.2);"
>
<TrendingUp class="h-5 w-5 text-ct" />
</div>
<div>
<div class="text-sm text-white/50">CT Full Buys</div>
<div class="text-3xl font-bold text-white">{teamB_fullBuys}</div>
</div>
</div> </div>
<div class="text-3xl font-bold text-base-content">{teamB_fullBuys}</div> <div class="mt-2 text-xs text-neon-red">{teamB_ecos} poverty rounds</div>
<div class="mt-1 text-xs text-base-content/60">{teamB_ecos} eco rounds</div>
</Card> </Card>
</div> </div>
<!-- Equipment Value Chart --> <!-- Equipment Value Chart -->
<Card padding="lg"> <Card padding="lg">
<div class="mb-4"> <div class="mb-4 flex items-center gap-3">
<h2 class="text-2xl font-bold text-base-content">Equipment Value Over Time</h2> <div
<p class="text-sm text-base-content/60"> class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-purple/20"
Total equipment value for each team across all rounds style="box-shadow: 0 0 15px rgba(139, 92, 246, 0.2);"
</p> >
<Wallet class="h-5 w-5 text-neon-purple" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">Equipment Value Over Time</h2>
<p class="text-sm text-white/50">Total equipment value for each team across all rounds</p>
</div>
</div> </div>
{#if equipmentChartData} {#if equipmentChartData}
<LineChart data={equipmentChartData} height={350} /> <LineChart
data={equipmentChartData}
height={350}
options={{
scales: {
y: {
grid: {
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: 'rgba(255, 255, 255, 0.5)'
}
},
x: {
grid: {
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: 'rgba(255, 255, 255, 0.5)'
}
}
},
plugins: {
legend: {
labels: {
color: 'rgba(255, 255, 255, 0.7)'
}
}
}
}}
/>
{/if} {/if}
</Card> </Card>
<!-- Round-by-Round Table --> <!-- Round-by-Round Table -->
<Card padding="none"> <Card padding="none">
<div class="p-6"> <div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Round-by-Round Economy</h2> <h2 class="text-2xl font-bold text-white">Round-by-Round Economy</h2>
<p class="mt-1 text-sm text-base-content/60"> <p class="mt-1 text-sm text-white/50">
Detailed breakdown of buy types and equipment values Detailed breakdown of buy types and equipment values - Where did all the money go?
</p> </p>
</div> </div>
@@ -382,24 +470,42 @@
</Card> </Card>
<!-- Buy Type Legend --> <!-- Buy Type Legend -->
<Card padding="lg"> <Card padding="lg" class="border-neon-blue/20">
<h3 class="mb-3 text-lg font-semibold text-base-content">Buy Type Classification</h3> <h3 class="mb-3 text-lg font-semibold text-white">
Buy Type Classification (A Financial Guide)
</h3>
<div class="flex flex-wrap gap-4 text-sm"> <div class="flex flex-wrap gap-4 text-sm">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Badge variant="error" size="sm">Eco</Badge> <span
<span class="text-base-content/60">&lt; $1,500 avg equipment</span> class="inline-flex items-center rounded-md border border-neon-red/30 bg-neon-red/20 px-2 py-0.5 text-xs font-medium text-neon-red"
>
Eco
</span>
<span class="text-white/50">&lt; $1,500 avg - "{buyTypeLabels['Eco']}"</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Badge variant="default" size="sm">Semi-Eco</Badge> <span
<span class="text-base-content/60">$1,500 - $2,500 avg equipment</span> class="inline-flex items-center rounded-md border border-white/20 bg-white/10 px-2 py-0.5 text-xs font-medium text-white/60"
>
Semi-Eco
</span>
<span class="text-white/50">$1,500 - $2,500 - "{buyTypeLabels['Semi-Eco']}"</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Badge variant="warning" size="sm">Force</Badge> <span
<span class="text-base-content/60">$2,500 - $3,500 avg equipment</span> class="inline-flex items-center rounded-md border border-neon-gold/30 bg-neon-gold/20 px-2 py-0.5 text-xs font-medium text-neon-gold"
>
Force
</span>
<span class="text-white/50">$2,500 - $3,500 - "{buyTypeLabels['Force']}"</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Badge variant="success" size="sm">Full Buy</Badge> <span
<span class="text-base-content/60">&gt; $3,500 avg equipment</span> class="inline-flex items-center rounded-md border border-neon-green/30 bg-neon-green/20 px-2 py-0.5 text-xs font-medium text-neon-green"
>
Full Buy
</span>
<span class="text-white/50">&gt; $3,500 - "{buyTypeLabels['Full Buy']}"</span>
</div> </div>
</div> </div>
</Card> </Card>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Eye, Zap, Users } from 'lucide-svelte'; import { Eye, Zap, Users, Skull, AlertTriangle, Lightbulb } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte'; import Card from '$lib/components/ui/Card.svelte';
import DataTable from '$lib/components/data-display/DataTable.svelte'; import DataTable from '$lib/components/data-display/DataTable.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -11,6 +11,7 @@
const flashStats = (match.players || []) const flashStats = (match.players || [])
.map((player) => ({ .map((player) => ({
name: player.name, name: player.name,
playerId: player.id,
team_id: player.team_id, team_id: player.team_id,
enemies_blinded: player.flash_total_enemy || 0, enemies_blinded: player.flash_total_enemy || 0,
teammates_blinded: player.flash_total_team || 0, teammates_blinded: player.flash_total_team || 0,
@@ -49,9 +50,15 @@
const teamATotals = calcTeamTotals(teamAFlashStats); const teamATotals = calcTeamTotals(teamAFlashStats);
const teamBTotals = calcTeamTotals(teamBFlashStats); const teamBTotals = calcTeamTotals(teamBFlashStats);
// Hall of Shame - players who flashed more teammates than enemies
const hallOfShame = flashStats
.filter((p) => p.teammates_blinded > p.enemies_blinded && p.teammates_blinded > 0)
.sort((a, b) => b.teammates_blinded - a.teammates_blinded);
// Table columns with fixed widths for consistency across multiple tables // Table columns with fixed widths for consistency across multiple tables
interface FlashStat { interface FlashStat {
name: string; name: string;
playerId: string;
team_id: number; team_id: number;
enemies_blinded: number; enemies_blinded: number;
teammates_blinded: number; teammates_blinded: number;
@@ -108,67 +115,144 @@
<!-- Summary Stats --> <!-- Summary Stats -->
<div class="grid gap-6 md:grid-cols-3"> <div class="grid gap-6 md:grid-cols-3">
<Card padding="lg"> <Card padding="lg">
<Eye class="mb-2 h-8 w-8 text-warning" /> <div class="flex items-center gap-3">
<div class="text-3xl font-bold text-base-content"> <div
{teamATotals.total_enemies_blinded + teamBTotals.total_enemies_blinded} class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-gold/20"
style="box-shadow: 0 0 20px rgba(255, 215, 0, 0.2);"
>
<Eye class="h-6 w-6 text-neon-gold" />
</div>
<div>
<div class="text-3xl font-bold text-white">
{teamATotals.total_enemies_blinded + teamBTotals.total_enemies_blinded}
</div>
<div class="text-sm text-white/60">Enemies Successfully Blinded</div>
</div>
</div> </div>
<div class="text-sm text-base-content/60">Enemies Successfully Blinded</div> <div class="mt-3 text-xs text-neon-green">The correct way to use flashes</div>
<div class="mt-1 text-xs text-success">The correct way to use flashes</div>
</Card> </Card>
<Card padding="lg"> <Card padding="lg">
<Zap class="mb-2 h-8 w-8 text-success" /> <div class="flex items-center gap-3">
<div class="text-3xl font-bold text-base-content"> <div
{teamATotals.total_flash_assists + teamBTotals.total_flash_assists} class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-green/20"
style="box-shadow: 0 0 20px rgba(0, 255, 136, 0.2);"
>
<Zap class="h-6 w-6 text-neon-green" />
</div>
<div>
<div class="text-3xl font-bold text-white">
{teamATotals.total_flash_assists + teamBTotals.total_flash_assists}
</div>
<div class="text-sm text-white/60">Flash Assists</div>
</div>
</div> </div>
<div class="text-sm text-base-content/60">Flash Assists</div> <div class="mt-3 text-xs text-neon-blue">Teamwork makes the dream work</div>
<div class="mt-1 text-xs text-success">Teamwork makes the dream work</div>
</Card> </Card>
<Card padding="lg"> <Card padding="lg">
<Users class="mb-2 h-8 w-8 text-error" /> <div class="flex items-center gap-3">
<div class="text-3xl font-bold text-base-content"> <div
{flashStats.reduce((sum, p) => sum + p.teammates_blinded, 0)} class="flex h-12 w-12 items-center justify-center rounded-lg bg-neon-red/20"
style="box-shadow: 0 0 20px rgba(255, 51, 102, 0.2);"
>
<Users class="h-6 w-6 text-neon-red" />
</div>
<div>
<div class="text-3xl font-bold text-white">
{flashStats.reduce((sum, p) => sum + p.teammates_blinded, 0)}
</div>
<div class="text-sm text-white/60">Teammates Betrayed</div>
</div>
</div> </div>
<div class="text-sm text-base-content/60">Teammates Betrayed</div> <div class="mt-3 text-xs text-neon-red">These players owe apologies</div>
<div class="mt-1 text-xs text-error">These players owe apologies</div>
</Card> </Card>
</div> </div>
<!-- Hall of Shame -->
{#if hallOfShame.length > 0}
<Card padding="lg" class="border-neon-red/30 bg-neon-red/5">
<div class="mb-4 flex items-center gap-3">
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-red/20"
style="box-shadow: 0 0 15px rgba(255, 51, 102, 0.3);"
>
<Skull class="h-5 w-5 text-neon-red" />
</div>
<div>
<h3 class="text-lg font-bold text-neon-red">Hall of Shame</h3>
<p class="text-xs text-white/50">Players who flashed more teammates than enemies</p>
</div>
</div>
<div class="space-y-3">
{#each hallOfShame as shamePlayer, index}
<div
class="flex items-center justify-between rounded-lg border border-neon-red/20 bg-void/50 px-4 py-3"
>
<div class="flex items-center gap-3">
<div
class="flex h-8 w-8 items-center justify-center rounded-full bg-neon-red/20 text-sm font-bold text-neon-red"
>
{index + 1}
</div>
<a
href={`/player/${shamePlayer.playerId}`}
class="font-medium text-white transition-colors hover:text-neon-blue"
>
{shamePlayer.name}
</a>
</div>
<div class="flex items-center gap-4 text-sm">
<div class="text-white/60">
<span class="text-neon-green">{shamePlayer.enemies_blinded}</span> enemies
</div>
<div class="text-neon-red">
<span class="font-bold">{shamePlayer.teammates_blinded}</span> teammates
</div>
</div>
</div>
{/each}
</div>
<p class="mt-4 text-center text-xs italic text-white/40">
Maybe consider switching to smokes?
</p>
</Card>
{/if}
<!-- Team Comparison --> <!-- Team Comparison -->
<div class="grid gap-6 md:grid-cols-2"> <div class="grid gap-6 md:grid-cols-2">
<Card padding="lg"> <Card padding="lg" class="border-l-4 border-l-terrorist">
<h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists</h3> <h3 class="mb-4 text-xl font-bold text-terrorist">Terrorists</h3>
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-sm text-base-content/60">Enemies Blinded</span> <span class="text-sm text-white/50">Enemies Blinded</span>
<span class="font-mono font-bold">{teamATotals.total_enemies_blinded}</span> <span class="font-mono font-bold text-white">{teamATotals.total_enemies_blinded}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-sm text-base-content/60">Flash Assists</span> <span class="text-sm text-white/50">Flash Assists</span>
<span class="font-mono font-bold">{teamATotals.total_flash_assists}</span> <span class="font-mono font-bold text-white">{teamATotals.total_flash_assists}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-sm text-base-content/60">Avg per Player</span> <span class="text-sm text-white/50">Avg per Player</span>
<span class="font-mono font-bold">{teamATotals.avg_per_player}</span> <span class="font-mono font-bold text-white">{teamATotals.avg_per_player}</span>
</div> </div>
</div> </div>
</Card> </Card>
<Card padding="lg"> <Card padding="lg" class="border-l-4 border-l-ct">
<h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists</h3> <h3 class="mb-4 text-xl font-bold text-ct">Counter-Terrorists</h3>
<div class="space-y-3"> <div class="space-y-3">
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-sm text-base-content/60">Enemies Blinded</span> <span class="text-sm text-white/50">Enemies Blinded</span>
<span class="font-mono font-bold">{teamBTotals.total_enemies_blinded}</span> <span class="font-mono font-bold text-white">{teamBTotals.total_enemies_blinded}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-sm text-base-content/60">Flash Assists</span> <span class="text-sm text-white/50">Flash Assists</span>
<span class="font-mono font-bold">{teamBTotals.total_flash_assists}</span> <span class="font-mono font-bold text-white">{teamBTotals.total_flash_assists}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-sm text-base-content/60">Avg per Player</span> <span class="text-sm text-white/50">Avg per Player</span>
<span class="font-mono font-bold">{teamBTotals.avg_per_player}</span> <span class="font-mono font-bold text-white">{teamBTotals.avg_per_player}</span>
</div> </div>
</div> </div>
</Card> </Card>
@@ -177,10 +261,18 @@
<!-- Flash Effectiveness Leaderboard --> <!-- Flash Effectiveness Leaderboard -->
<Card padding="none"> <Card padding="none">
<div class="p-6"> <div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Flash Hall of Fame (and Shame)</h2> <div class="flex items-center gap-3">
<p class="mt-1 text-sm text-base-content/60"> <AlertTriangle
Ranked by enemies blinded. Teammates blinded is tracked for... scientific purposes. class="h-6 w-6 text-neon-gold"
</p> style="filter: drop-shadow(0 0 8px rgba(255, 215, 0, 0.5));"
/>
<div>
<h2 class="text-2xl font-bold text-white">Flash Hall of Fame (and Shame)</h2>
<p class="mt-1 text-sm text-white/50">
Ranked by enemies blinded. Teammates blinded is tracked for... scientific purposes.
</p>
</div>
</div>
</div> </div>
<DataTable data={flashStats} {columns} striped hoverable fixedLayout /> <DataTable data={flashStats} {columns} striped hoverable fixedLayout />
@@ -188,7 +280,7 @@
<!-- Team A Details --> <!-- Team A Details -->
<Card padding="none"> <Card padding="none">
<div class="border-b border-base-300 bg-terrorist/5 p-6"> <div class="border-b border-white/10 bg-terrorist/10 p-6">
<h3 class="text-xl font-bold text-terrorist">Terrorists - Flash Stats</h3> <h3 class="text-xl font-bold text-terrorist">Terrorists - Flash Stats</h3>
</div> </div>
<DataTable data={teamAFlashStats} {columns} striped hoverable fixedLayout /> <DataTable data={teamAFlashStats} {columns} striped hoverable fixedLayout />
@@ -196,37 +288,69 @@
<!-- Team B Details --> <!-- Team B Details -->
<Card padding="none"> <Card padding="none">
<div class="border-b border-base-300 bg-ct/5 p-6"> <div class="border-b border-white/10 bg-ct/10 p-6">
<h3 class="text-xl font-bold text-ct">Counter-Terrorists - Flash Stats</h3> <h3 class="text-xl font-bold text-ct">Counter-Terrorists - Flash Stats</h3>
</div> </div>
<DataTable data={teamBFlashStats} {columns} striped hoverable fixedLayout /> <DataTable data={teamBFlashStats} {columns} striped hoverable fixedLayout />
</Card> </Card>
<!-- Info Box --> <!-- Info Box -->
<Card padding="lg" variant="elevated"> <Card padding="lg" variant="elevated" class="border-neon-blue/20">
<div class="text-sm text-base-content/60"> <div class="flex items-start gap-4">
<p class="mb-2 font-semibold">Flash Stats Explained (For the Visually Challenged):</p> <div
<ul class="list-inside list-disc space-y-1"> class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-neon-blue/20"
<li><strong>Victims (Correct):</strong> Enemies you blinded - the RIGHT people to flash</li> style="box-shadow: 0 0 15px rgba(0, 212, 255, 0.2);"
<li> >
<strong>Avg Suffering:</strong> Average time enemies spent regretting their peek <Lightbulb class="h-5 w-5 text-neon-blue" />
</li> </div>
<li> <div class="text-sm text-white/70">
<strong>Actually Useful:</strong> Enemies killed by teammates while your flash was doing its <p class="mb-3 font-semibold text-white">
job Flash Stats Explained (For the Visually Challenged):
</li> </p>
<li> <ul class="space-y-2">
<strong>Friendly Crimes:</strong> Number of times you betrayed your own team - shame counter <li class="flex items-start gap-2">
</li> <span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-green"></span>
<li> <span
<strong>Self-Inflicted L:</strong> Times you stared at your own flashbang like a moth to a ><strong class="text-neon-green">Victims (Correct):</strong> Enemies you blinded - the
flame RIGHT people to flash</span
</li> >
</ul> </li>
<p class="mt-4 text-xs italic"> <li class="flex items-start gap-2">
Remember: If your "Friendly Crimes" is higher than "Victims (Correct)", you might want to <span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-gold"></span>
reconsider your flash lineups. <span
</p> ><strong class="text-neon-gold">Avg Suffering:</strong> Average time enemies spent regretting
their peek</span
>
</li>
<li class="flex items-start gap-2">
<span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-blue"></span>
<span
><strong class="text-neon-blue">Actually Useful:</strong> Enemies killed by teammates while
your flash was doing its job</span
>
</li>
<li class="flex items-start gap-2">
<span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-red"></span>
<span
><strong class="text-neon-red">Friendly Crimes:</strong> Number of times you betrayed your
own team - shame counter</span
>
</li>
<li class="flex items-start gap-2">
<span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-neon-purple"></span>
<span
><strong class="text-neon-purple">Self-Inflicted L:</strong> Times you stared at your own
flashbang like a moth to a flame</span
>
</li>
</ul>
<p
class="mt-4 rounded-lg border border-neon-gold/20 bg-neon-gold/5 px-3 py-2 text-xs italic text-neon-gold"
>
Pro tip: If your "Friendly Crimes" is higher than "Victims (Correct)", you might want to
reconsider your flash lineups. Or your life choices.
</p>
</div>
</div> </div>
</Card> </Card>
</div> </div>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Crosshair, Target, AlertCircle, TrendingUp } from 'lucide-svelte'; import { Crosshair, Target, AlertCircle, TrendingUp, Swords } from 'lucide-svelte';
import Card from '$lib/components/ui/Card.svelte'; import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte'; import Badge from '$lib/components/ui/Badge.svelte';
import DataTable from '$lib/components/data-display/DataTable.svelte'; import DataTable from '$lib/components/data-display/DataTable.svelte';
@@ -64,29 +64,29 @@
render: (value: unknown, row: PlayerWeapon) => { render: (value: unknown, row: PlayerWeapon) => {
const strValue = value !== undefined ? String(value) : ''; const strValue = value !== undefined ? String(value) : '';
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct'; const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
return `<a href="/player/${row.player_id}" class="font-medium hover:underline ${teamClass}">${strValue}</a>`; return `<a href="/player/${row.player_id}" class="font-medium hover:text-neon-blue transition-colors ${teamClass}">${strValue}</a>`;
} }
}, },
{ {
key: 'top_weapon' as const, key: 'top_weapon' as const,
label: 'Top Weapon', label: 'Weapon of Choice',
sortable: true, sortable: true,
align: 'left' as const, align: 'left' as const,
class: 'font-medium' class: 'font-medium text-white'
}, },
{ {
key: 'total_kills' as const, key: 'total_kills' as const,
label: 'Total Kills', label: 'Total Kills',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono font-semibold' class: 'font-mono font-semibold text-white'
}, },
{ {
key: 'total_damage' as const, key: 'total_damage' as const,
label: 'Total Damage', label: 'Total Damage',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono', class: 'font-mono text-white/80',
format: (v: unknown) => (v !== undefined ? (v as number).toLocaleString() : '0') format: (v: unknown) => (v !== undefined ? (v as number).toLocaleString() : '0')
}, },
{ {
@@ -94,7 +94,7 @@
label: 'Total Hits', label: 'Total Hits',
sortable: true, sortable: true,
align: 'center' as const, align: 'center' as const,
class: 'font-mono' class: 'font-mono text-white/80'
} }
]; ];
@@ -111,7 +111,7 @@
existing.kills += ws.kills; existing.kills += ws.kills;
existing.damage += ws.damage; existing.damage += ws.damage;
existing.hits += ws.hits; existing.hits += ws.hits;
existing.headshot_pct = ws.headshot_pct || 0; // Use latest existing.headshot_pct = ws.headshot_pct || 0;
} else { } else {
weaponAggregates.set(ws.weapon_name, { weaponAggregates.set(ws.weapon_name, {
kills: ws.kills, kills: ws.kills,
@@ -129,19 +129,21 @@
.sort((a, b) => b.kills - a.kills) .sort((a, b) => b.kills - a.kills)
.slice(0, 10); .slice(0, 10);
// Weapon usage chart data // Weapon usage chart data with neon colors
const weaponUsageData = { const weaponUsageData = {
labels: topWeapons.map((w) => w.name), labels: topWeapons.map((w) => w.name),
datasets: [ datasets: [
{ {
label: 'Kills', label: 'Kills',
data: topWeapons.map((w) => w.kills), data: topWeapons.map((w) => w.kills),
backgroundColor: 'rgba(59, 130, 246, 0.8)' backgroundColor: 'rgba(0, 212, 255, 0.7)',
borderColor: '#00d4ff',
borderWidth: 1
} }
] ]
}; };
// Hit group distribution (aggregate across all weapons) // Hit group distribution with neon colors
const hitGroupTotals = { const hitGroupTotals = {
head: 0, head: 0,
chest: 0, chest: 0,
@@ -177,11 +179,11 @@
hitGroupTotals.left_leg + hitGroupTotals.right_leg hitGroupTotals.left_leg + hitGroupTotals.right_leg
], ],
backgroundColor: [ backgroundColor: [
'rgba(239, 68, 68, 0.8)', // Red for head 'rgba(255, 51, 102, 0.8)', // neon-red for head
'rgba(59, 130, 246, 0.8)', // Blue for chest 'rgba(0, 212, 255, 0.8)', // neon-blue for chest
'rgba(249, 115, 22, 0.8)', // Orange for stomach 'rgba(255, 215, 0, 0.8)', // neon-gold for stomach
'rgba(34, 197, 94, 0.8)', // Green for arms 'rgba(0, 255, 136, 0.8)', // neon-green for arms
'rgba(168, 85, 247, 0.8)' // Purple for legs 'rgba(139, 92, 246, 0.8)' // neon-purple for legs
] ]
} }
] ]
@@ -195,9 +197,14 @@
{#if !hasWeaponsData} {#if !hasWeaponsData}
<Card padding="lg"> <Card padding="lg">
<div class="text-center"> <div class="text-center">
<AlertCircle class="mx-auto mb-4 h-16 w-16 text-warning" /> <AlertCircle
<h2 class="mb-2 text-2xl font-bold text-base-content">No Weapons Data Available</h2> class="mx-auto mb-4 h-16 w-16 text-neon-gold"
<p class="mb-4 text-base-content/60">Weapon statistics are not available for this match.</p> style="filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.4));"
/>
<h2 class="mb-2 text-2xl font-bold text-white">No Weapons Data Available</h2>
<p class="mb-4 text-white/60">
Weapon statistics are not available for this match. The armory remains sealed.
</p>
<Badge variant="warning" size="lg">Weapons data unavailable</Badge> <Badge variant="warning" size="lg">Weapons data unavailable</Badge>
</div> </div>
</Card> </Card>
@@ -206,53 +213,125 @@
<!-- Top Stats Summary --> <!-- Top Stats Summary -->
<div class="grid gap-6 md:grid-cols-3"> <div class="grid gap-6 md:grid-cols-3">
<Card padding="lg"> <Card padding="lg">
<div class="mb-3 flex items-center gap-2"> <div class="flex items-center gap-3">
<Crosshair class="h-5 w-5 text-primary" /> <div
<h3 class="font-semibold text-base-content">Total Kills</h3> 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);"
>
<Crosshair class="h-5 w-5 text-neon-blue" />
</div>
<div>
<div class="text-sm text-white/50">Total Kills</div>
<div
class="font-mono text-3xl font-bold text-neon-blue"
style="text-shadow: 0 0 15px rgba(0, 212, 255, 0.4);"
>
{topWeapons.reduce((sum, w) => sum + w.kills, 0)}
</div>
</div>
</div> </div>
<div class="font-mono text-3xl font-bold text-primary"> <div class="mt-2 text-xs text-white/40">Across all weapons</div>
{topWeapons.reduce((sum, w) => sum + w.kills, 0)}
</div>
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
</Card> </Card>
<Card padding="lg"> <Card padding="lg">
<div class="mb-3 flex items-center gap-2"> <div class="flex items-center gap-3">
<Target class="h-5 w-5 text-success" /> <div
<h3 class="font-semibold text-base-content">Total Damage</h3> 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);"
>
<Target class="h-5 w-5 text-neon-green" />
</div>
<div>
<div class="text-sm text-white/50">Total Damage</div>
<div
class="font-mono text-3xl font-bold text-neon-green"
style="text-shadow: 0 0 15px rgba(0, 255, 136, 0.4);"
>
{topWeapons.reduce((sum, w) => sum + w.damage, 0).toLocaleString()}
</div>
</div>
</div> </div>
<div class="font-mono text-3xl font-bold text-success"> <div class="mt-2 text-xs text-white/40">Across all weapons</div>
{topWeapons.reduce((sum, w) => sum + w.damage, 0).toLocaleString()}
</div>
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
</Card> </Card>
<Card padding="lg"> <Card padding="lg">
<div class="mb-3 flex items-center gap-2"> <div class="flex items-center gap-3">
<TrendingUp class="h-5 w-5 text-warning" /> <div
<h3 class="font-semibold text-base-content">Total Hits</h3> 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);"
>
<TrendingUp class="h-5 w-5 text-neon-gold" />
</div>
<div>
<div class="text-sm text-white/50">Total Hits</div>
<div
class="font-mono text-3xl font-bold text-neon-gold"
style="text-shadow: 0 0 15px rgba(255, 215, 0, 0.4);"
>
{topWeapons.reduce((sum, w) => sum + w.hits, 0).toLocaleString()}
</div>
</div>
</div> </div>
<div class="font-mono text-3xl font-bold text-warning"> <div class="mt-2 text-xs text-white/40">Across all weapons</div>
{topWeapons.reduce((sum, w) => sum + w.hits, 0).toLocaleString()}
</div>
<div class="mt-2 text-xs text-base-content/60">Across all weapons</div>
</Card> </Card>
</div> </div>
<!-- Top Weapons Chart --> <!-- Top Weapons Chart -->
<Card padding="lg"> <Card padding="lg">
<div class="mb-4"> <div class="mb-4 flex items-center gap-3">
<h2 class="text-2xl font-bold text-base-content">Most Used Weapons</h2> <div
<p class="text-sm text-base-content/60">Weapons ranked by total kills</p> class="flex h-10 w-10 items-center justify-center rounded-lg bg-neon-purple/20"
style="box-shadow: 0 0 15px rgba(139, 92, 246, 0.2);"
>
<Swords class="h-5 w-5 text-neon-purple" />
</div>
<div>
<h2 class="text-2xl font-bold text-white">The Arsenal Rankings</h2>
<p class="text-sm text-white/50">
Weapons ranked by total kills - The tools of destruction
</p>
</div>
</div> </div>
<BarChart data={weaponUsageData} height={300} /> <BarChart
data={weaponUsageData}
height={300}
options={{
scales: {
y: {
grid: {
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: 'rgba(255, 255, 255, 0.5)'
}
},
x: {
grid: {
color: 'rgba(255, 255, 255, 0.05)'
},
ticks: {
color: 'rgba(255, 255, 255, 0.5)'
}
}
},
plugins: {
legend: {
labels: {
color: 'rgba(255, 255, 255, 0.7)'
}
}
}
}}
/>
</Card> </Card>
<!-- Hit Group Distribution --> <!-- Hit Group Distribution -->
<Card padding="lg"> <Card padding="lg">
<div class="mb-4"> <div class="mb-4">
<h2 class="text-2xl font-bold text-base-content">Hit Location Distribution</h2> <h2 class="text-2xl font-bold text-white">Hit Location Distribution</h2>
<p class="text-sm text-base-content/60">Where shots landed across all weapons</p> <p class="text-sm text-white/50">
Where shots landed across all weapons - Anatomy of aggression
</p>
</div> </div>
<div class="grid gap-6 md:grid-cols-2"> <div class="grid gap-6 md:grid-cols-2">
<PieChart data={hitGroupData} height={300} /> <PieChart data={hitGroupData} height={300} />
@@ -260,40 +339,40 @@
<div class="space-y-3"> <div class="space-y-3">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<div class="h-4 w-4 rounded bg-[rgba(239,68,68,0.8)]"></div> <div class="h-4 w-4 rounded bg-neon-red"></div>
<span>Head</span> <span class="text-white/80">Head</span>
</span> </span>
<span class="font-mono font-semibold">{hitGroupTotals.head}</span> <span class="font-mono font-semibold text-white">{hitGroupTotals.head}</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<div class="h-4 w-4 rounded bg-[rgba(59,130,246,0.8)]"></div> <div class="h-4 w-4 rounded bg-neon-blue"></div>
<span>Chest</span> <span class="text-white/80">Chest</span>
</span> </span>
<span class="font-mono font-semibold">{hitGroupTotals.chest}</span> <span class="font-mono font-semibold text-white">{hitGroupTotals.chest}</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<div class="h-4 w-4 rounded bg-[rgba(249,115,22,0.8)]"></div> <div class="h-4 w-4 rounded bg-neon-gold"></div>
<span>Stomach</span> <span class="text-white/80">Stomach</span>
</span> </span>
<span class="font-mono font-semibold">{hitGroupTotals.stomach}</span> <span class="font-mono font-semibold text-white">{hitGroupTotals.stomach}</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<div class="h-4 w-4 rounded bg-[rgba(34,197,94,0.8)]"></div> <div class="h-4 w-4 rounded bg-neon-green"></div>
<span>Arms</span> <span class="text-white/80">Arms</span>
</span> </span>
<span class="font-mono font-semibold" <span class="font-mono font-semibold text-white"
>{hitGroupTotals.left_arm + hitGroupTotals.right_arm}</span >{hitGroupTotals.left_arm + hitGroupTotals.right_arm}</span
> >
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<div class="h-4 w-4 rounded bg-[rgba(168,85,247,0.8)]"></div> <div class="h-4 w-4 rounded bg-neon-purple"></div>
<span>Legs</span> <span class="text-white/80">Legs</span>
</span> </span>
<span class="font-mono font-semibold" <span class="font-mono font-semibold text-white"
>{hitGroupTotals.left_leg + hitGroupTotals.right_leg}</span >{hitGroupTotals.left_leg + hitGroupTotals.right_leg}</span
> >
</div> </div>
@@ -305,8 +384,10 @@
<!-- Player Weapons Table --> <!-- Player Weapons Table -->
<Card padding="none"> <Card padding="none">
<div class="p-6"> <div class="p-6">
<h2 class="text-2xl font-bold text-base-content">Player Weapon Performance</h2> <h2 class="text-2xl font-bold text-white">Player Weapon Performance</h2>
<p class="mt-1 text-sm text-base-content/60">Individual player weapon statistics</p> <p class="mt-1 text-sm text-white/50">
Individual player weapon statistics - Who brought what to the fight
</p>
</div> </div>
<DataTable data={sortedPlayerWeapons} columns={weaponColumns} striped hoverable /> <DataTable data={sortedPlayerWeapons} columns={weaponColumns} striped hoverable />