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

@@ -0,0 +1,87 @@
<script lang="ts">
import { X } from 'lucide-svelte';
import { fly, fade } from 'svelte/transition';
interface Props {
open?: boolean;
title?: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
onClose?: () => void;
children?: any;
}
let { open = $bindable(false), title, size = 'md', onClose, children }: Props = $props();
const sizeClasses = {
sm: 'max-w-md',
md: 'max-w-2xl',
lg: 'max-w-4xl',
xl: 'max-w-6xl'
};
const handleClose = () => {
open = false;
onClose?.();
};
const handleBackdropClick = (e: MouseEvent) => {
if (e.target === e.currentTarget) {
handleClose();
}
};
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && open) {
handleClose();
}
};
</script>
<svelte:window onkeydown={handleKeydown} />
{#if open}
<div
class="fixed inset-0 z-50 flex items-center justify-center p-4"
transition:fade={{ duration: 200 }}
onclick={handleBackdropClick}
role="dialog"
aria-modal="true"
aria-labelledby={title ? 'modal-title' : undefined}
>
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm"></div>
<!-- Modal -->
<div
class="relative w-full {sizeClasses[size]} rounded-lg bg-base-100 shadow-xl"
transition:fly={{ y: -20, duration: 300 }}
>
<!-- Header -->
{#if title}
<div class="flex items-center justify-between border-b border-base-300 p-6">
<h2 id="modal-title" class="text-2xl font-bold text-base-content">{title}</h2>
<button
class="btn btn-circle btn-ghost btn-sm"
onclick={handleClose}
aria-label="Close modal"
>
<X class="h-5 w-5" />
</button>
</div>
{:else}
<button
class="btn btn-circle btn-ghost btn-sm absolute right-4 top-4 z-10"
onclick={handleClose}
aria-label="Close modal"
>
<X class="h-5 w-5" />
</button>
{/if}
<!-- Content -->
<div class="p-6">
{@render children?.()}
</div>
</div>
</div>
{/if}

View File

@@ -0,0 +1,26 @@
<script lang="ts">
interface Props {
variant?: 'text' | 'circular' | 'rectangular';
width?: string;
height?: string;
class?: string;
}
let { variant = 'rectangular', width, height, class: className = '' }: Props = $props();
const baseClasses = 'animate-pulse bg-base-300';
const variantClasses = {
text: 'rounded h-4',
circular: 'rounded-full',
rectangular: 'rounded'
};
const style = [width ? `width: ${width};` : '', height ? `height: ${height};` : '']
.filter(Boolean)
.join(' ');
</script>
<div class="{baseClasses} {variantClasses[variant]} {className}" {style} role="status">
<span class="sr-only">Loading...</span>
</div>

View File

@@ -0,0 +1,77 @@
<script lang="ts">
import { page } from '$app/stores';
interface Tab {
label: string;
href?: string;
value?: string;
disabled?: boolean;
}
interface Props {
tabs: Tab[];
activeTab?: string;
onTabChange?: (value: string) => void;
variant?: 'boxed' | 'bordered' | 'lifted';
size?: 'xs' | 'sm' | 'md' | 'lg';
class?: string;
}
let {
tabs,
activeTab = $bindable(),
onTabChange,
variant = 'bordered',
size = 'md',
class: className = ''
}: Props = $props();
// If using href-based tabs, derive active from current route
const isActive = (tab: Tab): boolean => {
if (tab.href) {
return $page.url.pathname === tab.href || $page.url.pathname.startsWith(tab.href + '/');
}
return activeTab === tab.value;
};
const handleTabClick = (tab: Tab) => {
if (tab.disabled) return;
if (tab.value && !tab.href) {
activeTab = tab.value;
onTabChange?.(tab.value);
}
};
const variantClass = variant === 'boxed' ? 'tabs-boxed' : variant === 'lifted' ? 'tabs-lifted' : '';
const sizeClass = size === 'xs' ? 'tabs-xs' : size === 'sm' ? 'tabs-sm' : size === 'lg' ? 'tabs-lg' : '';
</script>
<div role="tablist" class="tabs {variantClass} {sizeClass} {className}">
{#each tabs as tab}
{#if tab.href}
<a
href={tab.href}
role="tab"
class="tab"
class:tab-active={isActive(tab)}
class:tab-disabled={tab.disabled}
aria-disabled={tab.disabled}
data-sveltekit-preload-data="hover"
>
{tab.label}
</a>
{:else}
<button
role="tab"
class="tab"
class:tab-active={isActive(tab)}
class:tab-disabled={tab.disabled}
disabled={tab.disabled}
onclick={() => handleTabClick(tab)}
>
{tab.label}
</button>
{/if}
{/each}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
interface Props {
text: string;
position?: 'top' | 'bottom' | 'left' | 'right';
children?: any;
}
let { text, position = 'top', children }: Props = $props();
const positionClass = {
top: 'tooltip-top',
bottom: 'tooltip-bottom',
left: 'tooltip-left',
right: 'tooltip-right'
};
</script>
<div class="tooltip {positionClass[position]}" data-tip={text}>
{@render children?.()}
</div>