Files
csgowtf/src/lib/components/data-display/DataTable.svelte
vikingowl 6dc12f0c35 feat: Redesign matches page with neon styling and UX improvements
- Convert matches page from DaisyUI to neon esports design system
- Add colored left borders to cards for instant win/loss/tie scanning
- Add player count badges and demo status icons to match cards
- Implement filter state preservation across navigation
- Add staggered card animations and skeleton loading states
- Add slide transition for filter panel
- Make cards compact with horizontal layout for better density
- Update grid to 4 columns on xl screens
- Style DataTable, ShareCodeInput with neon theme
- Add external link support to NeonButton

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-07 17:11:19 +01:00

186 lines
4.6 KiB
Svelte

<script lang="ts" generics="T">
/* eslint-disable no-undef */
import { ArrowUp, ArrowDown } from 'lucide-svelte';
interface Column<T> {
key: keyof T;
label: string;
sortable?: boolean;
format?: (value: T[keyof T], row: T) => string;
render?: (value: T[keyof T], row: T) => unknown;
align?: 'left' | 'center' | 'right';
class?: string;
width?: string;
}
interface Props {
data: T[];
columns: Column<T>[];
class?: string;
striped?: boolean;
hoverable?: boolean;
compact?: boolean;
fixedLayout?: boolean;
}
let {
data,
columns,
class: className = '',
striped = false,
hoverable = true,
compact = false,
fixedLayout = false
}: Props = $props();
let sortKey = $state<keyof T | null>(null);
let sortDirection = $state<'asc' | 'desc'>('asc');
const handleSort = (column: Column<T>) => {
if (!column.sortable) return;
if (sortKey === column.key) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortKey = column.key;
sortDirection = 'asc';
}
};
const sortedData = $derived(
!sortKey
? data
: [...data].sort((a, b) => {
const aVal = a[sortKey as keyof T];
const bVal = b[sortKey as keyof T];
if (aVal === bVal) return 0;
const comparison = aVal < bVal ? -1 : 1;
return sortDirection === 'asc' ? comparison : -comparison;
})
);
const getValue = (row: T, column: Column<T>) => {
const value = row[column.key];
if (column.format) {
return column.format(value, row);
}
return value;
};
</script>
<div class="overflow-x-auto {className}">
<table class="w-full border-collapse" style={fixedLayout ? 'table-layout: fixed;' : ''}>
<thead>
<tr class="border-b border-white/10 bg-void">
{#each columns as column}
<th
class="px-4 text-left text-xs font-semibold uppercase tracking-wider text-white/60 {compact
? 'py-2'
: 'py-3'} {column.sortable
? 'cursor-pointer transition-colors hover:bg-neon-blue/10 hover:text-neon-blue'
: ''} {column.class || ''}"
class:text-center={column.align === 'center'}
class:text-right={column.align === 'right'}
style={column.width ? `width: ${column.width}` : ''}
onclick={() => handleSort(column)}
>
<div
class="flex items-center gap-2"
class:justify-end={column.align === 'right'}
class:justify-center={column.align === 'center'}
>
<span>{column.label}</span>
{#if column.sortable}
<div class="flex flex-col">
<ArrowUp
class="h-3 w-3 {sortKey === column.key && sortDirection === 'asc'
? 'text-neon-blue'
: 'text-white/30'}"
/>
<ArrowDown
class="-mt-1 h-3 w-3 {sortKey === column.key && sortDirection === 'desc'
? 'text-neon-blue'
: 'text-white/30'}"
/>
</div>
{/if}
</div>
</th>
{/each}
</tr>
</thead>
<tbody>
{#each sortedData as row, index}
<tr
class="border-b border-white/5 transition-colors {hoverable
? 'hover:bg-neon-blue/5'
: ''} {striped && index % 2 === 1 ? 'bg-white/[0.02]' : ''}"
>
{#each columns as column}
<td
class="px-4 text-white/80 {compact ? 'py-2' : 'py-3'} {column.class || ''}"
class:text-center={column.align === 'center'}
class:text-right={column.align === 'right'}
>
{#if column.render}
{@html column.render(row[column.key], row)}
{:else}
{getValue(row, column)}
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
<style>
/* Style links and buttons within table cells */
:global(td a) {
color: rgb(0, 212, 255);
transition: color 0.2s;
}
:global(td a:hover) {
color: rgb(0, 170, 204);
}
:global(td .btn-primary) {
background-color: rgb(0, 212, 255);
color: rgb(10, 10, 15);
border: none;
padding: 0.375rem 0.75rem;
border-radius: 0.5rem;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.2s;
}
:global(td .btn-primary:hover) {
box-shadow: 0 0 15px rgba(0, 212, 255, 0.4);
transform: scale(1.02);
}
/* Neon badge styling for result badges */
:global(td .badge-success) {
background-color: rgba(0, 255, 136, 0.1);
color: rgb(0, 255, 136);
border: 1px solid rgba(0, 255, 136, 0.3);
}
:global(td .badge-error) {
background-color: rgba(255, 51, 102, 0.1);
color: rgb(255, 51, 102);
border: 1px solid rgba(255, 51, 102, 0.3);
}
:global(td .badge-warning) {
background-color: rgba(255, 215, 0, 0.1);
color: rgb(255, 215, 0);
border: 1px solid rgba(255, 215, 0, 0.3);
}
:global(td .badge) {
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
</style>