forked from CSGOWTF/csgowtf
- 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>
186 lines
4.6 KiB
Svelte
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>
|