feat: Implement Phase 5 match detail tabs with charts and data visualization

This commit implements significant portions of Phase 5 (Feature Delivery) including:

Chart Components (src/lib/components/charts/):
- LineChart.svelte: Line charts with Chart.js integration
- BarChart.svelte: Vertical/horizontal bar charts with stacking
- PieChart.svelte: Pie/Doughnut charts with legend
- All charts use Svelte 5 runes ($effect) for reactivity
- Responsive design with customizable options

Data Display Components (src/lib/components/data-display/):
- DataTable.svelte: Generic sortable, filterable table component
- TypeScript generics support for type safety
- Custom formatters and renderers
- Sort indicators and column alignment options

Match Detail Pages:
- Match layout with header, tabs, and score display
- Economy tab: Equipment value charts, buy type classification, round-by-round table
- Details tab: Multi-kill distribution charts, team performance, top performers
- Chat tab: Chronological messages with filtering, search, and round grouping

Additional Components:
- SearchBar, ThemeToggle (layout components)
- MatchCard, PlayerCard (domain components)
- Modal, Skeleton, Tabs, Tooltip (UI components)
- Player profile page with stats and recent matches

Dependencies:
- Installed chart.js for data visualization
- Created Svelte 5 compatible chart wrappers

Phase 4 marked as complete, Phase 5 at 50% completion.
Flashes and Damage tabs deferred for future implementation.

Note: Minor linting warnings to be addressed in follow-up commit.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-04 21:17:32 +01:00
parent 24b990ac62
commit 523136ffbc
30 changed files with 11721 additions and 9195 deletions

View File

@@ -1,8 +1,33 @@
<script lang="ts">
import { Search, Filter } from 'lucide-svelte';
import { Search, Filter, Calendar } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import Button from '$lib/components/ui/Button.svelte';
import Card from '$lib/components/ui/Card.svelte';
import Badge from '$lib/components/ui/Badge.svelte';
import MatchCard from '$lib/components/match/MatchCard.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
// Extract current filters from URL
const currentMap = $page.url.searchParams.get('map') || '';
const currentPlayerId = $page.url.searchParams.get('player_id') || '';
const currentSearch = $page.url.searchParams.get('search') || '';
let searchQuery = $state(currentSearch);
let showFilters = $state(false);
const handleSearch = () => {
const params = new URLSearchParams();
if (searchQuery) params.set('search', searchQuery);
if (currentMap) params.set('map', currentMap);
if (currentPlayerId) params.set('player_id', currentPlayerId);
goto(`/matches?${params.toString()}`);
};
const commonMaps = ['de_dust2', 'de_mirage', 'de_inferno', 'de_nuke', 'de_overpass', 'de_ancient', 'de_anubis'];
</script>
<svelte:head>
@@ -17,34 +42,93 @@
<!-- Search & Filters -->
<Card padding="lg" class="mb-8">
<div class="flex flex-col gap-4 md:flex-row">
<div class="flex-1">
<div class="relative">
<Search class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-base-content/40" />
<input
type="text"
placeholder="Search by player name, match ID, or share code..."
class="input input-bordered w-full pl-10"
/>
<form onsubmit={(e) => { e.preventDefault(); handleSearch(); }} class="flex flex-col gap-4">
<div class="flex flex-col gap-4 md:flex-row">
<div class="flex-1">
<div class="relative">
<Search class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-base-content/40" />
<input
bind:value={searchQuery}
type="text"
placeholder="Search by player name, match ID, or share code..."
class="input input-bordered w-full pl-10"
/>
</div>
</div>
<Button type="submit" variant="primary">
<Search class="mr-2 h-5 w-5" />
Search
</Button>
<Button type="button" variant="ghost" onclick={() => (showFilters = !showFilters)}>
<Filter class="mr-2 h-5 w-5" />
Filters
</Button>
</div>
<Button variant="ghost">
<Filter class="mr-2 h-5 w-5" />
Filters
</Button>
</div>
<!-- Filter Panel (Collapsible) -->
{#if showFilters}
<div class="border-t border-base-300 pt-4">
<h3 class="mb-3 font-semibold text-base-content">Filter by Map</h3>
<div class="flex flex-wrap gap-2">
{#each commonMaps as mapName}
<a
href={`/matches?map=${mapName}`}
class="badge badge-lg badge-outline hover:badge-primary"
class:badge-primary={currentMap === mapName}
>
{mapName}
</a>
{/each}
</div>
</div>
{/if}
</form>
<!-- Active Filters -->
{#if currentMap || currentPlayerId || currentSearch}
<div class="mt-4 flex flex-wrap items-center gap-2 border-t border-base-300 pt-4">
<span class="text-sm font-medium text-base-content/70">Active Filters:</span>
{#if currentSearch}
<Badge variant="info">Search: {currentSearch}</Badge>
{/if}
{#if currentMap}
<Badge variant="info">Map: {currentMap}</Badge>
{/if}
{#if currentPlayerId}
<Badge variant="info">Player ID: {currentPlayerId}</Badge>
{/if}
<Button variant="ghost" size="sm" href="/matches">Clear All</Button>
</div>
{/if}
</Card>
<!-- Coming Soon -->
<div
class="flex min-h-[400px] items-center justify-center rounded-lg border-2 border-dashed border-base-300 bg-base-200/50"
>
<div class="text-center">
<h2 class="mb-2 text-2xl font-bold text-base-content">Coming Soon</h2>
<p class="text-base-content/60">Match listings will be available in Phase 3</p>
<div class="mt-6">
<Badge variant="info">Phase 3 - In Development</Badge>
</div>
<!-- Matches Grid -->
{#if data.matches.length > 0}
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{#each data.matches as match}
<MatchCard {match} />
{/each}
</div>
</div>
{#if data.hasMore}
<div class="mt-8 text-center">
<Badge variant="info">More matches available - pagination coming soon</Badge>
</div>
{/if}
{:else}
<Card padding="lg">
<div class="text-center">
<Calendar class="mx-auto mb-4 h-16 w-16 text-base-content/40" />
<h2 class="mb-2 text-xl font-semibold text-base-content">No Matches Found</h2>
<p class="text-base-content/60">
Try adjusting your filters or search query.
</p>
{#if currentMap || currentPlayerId || currentSearch}
<div class="mt-4">
<Button variant="primary" href="/matches">View All Matches</Button>
</div>
{/if}
</div>
</Card>
{/if}
</div>