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>
This commit is contained in:
17
src/app.css
17
src/app.css
@@ -129,6 +129,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Card fade-in animation with stagger support */
|
||||||
|
.animate-card-in {
|
||||||
|
animation: cardFadeIn 0.4s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cardFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Neon Text Glow Effects */
|
/* Neon Text Glow Effects */
|
||||||
.text-glow-sm {
|
.text-glow-sm {
|
||||||
text-shadow: 0 0 10px currentColor;
|
text-shadow: 0 0 10px currentColor;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
render?: (value: T[keyof T], row: T) => unknown;
|
render?: (value: T[keyof T], row: T) => unknown;
|
||||||
align?: 'left' | 'center' | 'right';
|
align?: 'left' | 'center' | 'right';
|
||||||
class?: string;
|
class?: string;
|
||||||
width?: string; // e.g., '200px', '30%', 'auto'
|
width?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
striped?: boolean;
|
striped?: boolean;
|
||||||
hoverable?: boolean;
|
hoverable?: boolean;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
fixedLayout?: boolean; // Use table-layout: fixed for consistent column widths
|
fixedLayout?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -71,19 +71,18 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="overflow-x-auto {className}">
|
<div class="overflow-x-auto {className}">
|
||||||
<table
|
<table class="w-full border-collapse" style={fixedLayout ? 'table-layout: fixed;' : ''}>
|
||||||
class="table"
|
|
||||||
class:table-zebra={striped}
|
|
||||||
class:table-xs={compact}
|
|
||||||
style={fixedLayout ? 'table-layout: fixed;' : ''}
|
|
||||||
>
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr class="border-b border-white/10 bg-void">
|
||||||
{#each columns as column}
|
{#each columns as column}
|
||||||
<th
|
<th
|
||||||
class:cursor-pointer={column.sortable}
|
class="px-4 text-left text-xs font-semibold uppercase tracking-wider text-white/60 {compact
|
||||||
class:hover:bg-base-200={column.sortable}
|
? 'py-2'
|
||||||
class="text-{column.align || 'left'} {column.class || ''}"
|
: '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}` : ''}
|
style={column.width ? `width: ${column.width}` : ''}
|
||||||
onclick={() => handleSort(column)}
|
onclick={() => handleSort(column)}
|
||||||
>
|
>
|
||||||
@@ -94,16 +93,16 @@
|
|||||||
>
|
>
|
||||||
<span>{column.label}</span>
|
<span>{column.label}</span>
|
||||||
{#if column.sortable}
|
{#if column.sortable}
|
||||||
<div class="flex flex-col opacity-40">
|
<div class="flex flex-col">
|
||||||
<ArrowUp
|
<ArrowUp
|
||||||
class="h-3 w-3 {sortKey === column.key && sortDirection === 'asc'
|
class="h-3 w-3 {sortKey === column.key && sortDirection === 'asc'
|
||||||
? 'text-primary opacity-100'
|
? 'text-neon-blue'
|
||||||
: ''}"
|
: 'text-white/30'}"
|
||||||
/>
|
/>
|
||||||
<ArrowDown
|
<ArrowDown
|
||||||
class="-mt-1 h-3 w-3 {sortKey === column.key && sortDirection === 'desc'
|
class="-mt-1 h-3 w-3 {sortKey === column.key && sortDirection === 'desc'
|
||||||
? 'text-primary opacity-100'
|
? 'text-neon-blue'
|
||||||
: ''}"
|
: 'text-white/30'}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -113,10 +112,18 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each sortedData as row}
|
{#each sortedData as row, index}
|
||||||
<tr class:hover={hoverable}>
|
<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}
|
{#each columns as column}
|
||||||
<td class="text-{column.align || 'left'} {column.class || ''}">
|
<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}
|
{#if column.render}
|
||||||
{@html column.render(row[column.key], row)}
|
{@html column.render(row[column.key], row)}
|
||||||
{:else}
|
{:else}
|
||||||
@@ -129,3 +136,50 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
Browse Matches
|
Browse Matches
|
||||||
</NeonButton>
|
</NeonButton>
|
||||||
|
|
||||||
<NeonButton href="https://somegit.dev/CSGOWTF/csgowtf" variant="gold" size="lg">
|
<NeonButton href="https://somegit.dev/CSGOWTF/csgowtf" variant="gold" size="lg" external>
|
||||||
<Github class="mr-2 h-5 w-5" aria-hidden="true" />
|
<Github class="mr-2 h-5 w-5" aria-hidden="true" />
|
||||||
View on GitHub
|
View on GitHub
|
||||||
</NeonButton>
|
</NeonButton>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
import { CheckCircle2, Clock } from 'lucide-svelte';
|
||||||
import type { MatchListItem } from '$lib/types';
|
import type { MatchListItem } from '$lib/types';
|
||||||
import { storeMatchesState } from '$lib/utils/navigation';
|
import { storeMatchesState, type FilterState } from '$lib/utils/navigation';
|
||||||
import { getMapBackground, formatMapName } from '$lib/utils/mapAssets';
|
import { getMapBackground, formatMapName } from '$lib/utils/mapAssets';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
match: MatchListItem;
|
match: MatchListItem;
|
||||||
loadedCount?: number;
|
loadedCount?: number;
|
||||||
|
filters?: FilterState;
|
||||||
|
index?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { match, loadedCount = 0 }: Props = $props();
|
let { match, loadedCount = 0, filters, index = 0 }: Props = $props();
|
||||||
|
|
||||||
const formattedDate = new Date(match.date).toLocaleString('en-US', {
|
const formattedDate = new Date(match.date).toLocaleString('en-US', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@@ -21,9 +23,28 @@
|
|||||||
const mapName = formatMapName(match.map);
|
const mapName = formatMapName(match.map);
|
||||||
const mapBg = getMapBackground(match.map);
|
const mapBg = getMapBackground(match.map);
|
||||||
|
|
||||||
|
// Derive match result for colored border
|
||||||
|
const matchResult = $derived.by(() => {
|
||||||
|
if (match.score_team_a > match.score_team_b) return 'win';
|
||||||
|
if (match.score_team_a < match.score_team_b) return 'loss';
|
||||||
|
return 'tie';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Border color class based on result
|
||||||
|
const resultBorderColor = $derived.by(() => {
|
||||||
|
const colors = {
|
||||||
|
win: 'border-l-neon-green',
|
||||||
|
loss: 'border-l-neon-red',
|
||||||
|
tie: 'border-l-neon-gold'
|
||||||
|
};
|
||||||
|
return colors[matchResult];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stagger delay for animation (cap at 20 items per batch)
|
||||||
|
const staggerDelay = $derived(`${Math.min(index % 20, 19) * 50}ms`);
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
// Store navigation state before navigating
|
storeMatchesState(match.match_id, loadedCount, filters);
|
||||||
storeMatchesState(match.match_id, loadedCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleImageError(event: Event) {
|
function handleImageError(event: Event) {
|
||||||
@@ -34,66 +55,86 @@
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
href={`/match/${match.match_id}`}
|
href={`/match/${match.match_id}`}
|
||||||
class="block transition-transform hover:scale-[1.02]"
|
class="animate-card-in group block transition-transform duration-300 hover:scale-[1.01] motion-reduce:animate-none motion-reduce:hover:scale-100"
|
||||||
|
style="animation-delay: {staggerDelay};"
|
||||||
data-match-id={match.match_id}
|
data-match-id={match.match_id}
|
||||||
onclick={handleClick}
|
onclick={handleClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="overflow-hidden rounded-lg border border-base-300 bg-base-100 shadow-md transition-shadow hover:shadow-xl"
|
class="overflow-hidden rounded-lg border border-l-4 border-white/10 {resultBorderColor} bg-void-light transition-all duration-300 group-hover:border-l-4 group-hover:border-neon-blue/50 group-hover:{resultBorderColor} group-hover:shadow-[0_0_20px_rgba(0,212,255,0.1)]"
|
||||||
>
|
>
|
||||||
<!-- Map Header with Background Image -->
|
<!-- Map Header with Background Image -->
|
||||||
<div class="relative h-32 overflow-hidden">
|
<div class="relative h-20 overflow-hidden">
|
||||||
<!-- Background Image -->
|
<!-- Background Image -->
|
||||||
<img
|
<img
|
||||||
src={mapBg}
|
src={mapBg}
|
||||||
alt={mapName}
|
alt={mapName}
|
||||||
class="absolute inset-0 h-full w-full object-cover"
|
class="absolute inset-0 h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onerror={handleImageError}
|
onerror={handleImageError}
|
||||||
/>
|
/>
|
||||||
<!-- Overlay for better text contrast -->
|
<!-- Overlay for better text contrast -->
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-black/20"></div>
|
<div class="absolute inset-0 bg-gradient-to-t from-void via-void/70 to-transparent"></div>
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="relative flex h-full items-end justify-between p-3">
|
<div class="relative flex h-full items-end justify-between p-2">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex items-center gap-2">
|
||||||
{#if match.map}
|
<span
|
||||||
<Badge variant="default">{match.map}</Badge>
|
class="text-sm font-bold text-white drop-shadow-lg"
|
||||||
{/if}
|
style="text-shadow: 0 0 8px rgba(0, 212, 255, 0.3);"
|
||||||
<span class="text-lg font-bold text-white drop-shadow-lg">{mapName}</span>
|
>
|
||||||
|
{mapName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Status badges - horizontal layout -->
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
{#if match.player_count}
|
||||||
|
<span
|
||||||
|
class="rounded-full border border-neon-blue/30 bg-neon-blue/20 px-1.5 py-0.5 text-[10px] text-neon-blue backdrop-blur-sm"
|
||||||
|
title="{match.player_count} players"
|
||||||
|
>
|
||||||
|
{match.player_count >= 10 ? '5v5' : `${match.player_count}p`}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-0.5 rounded-full border border-white/10 bg-void/50 px-1.5 py-0.5 backdrop-blur-sm"
|
||||||
|
title={match.demo_parsed ? 'Demo fully parsed' : 'Demo processing'}
|
||||||
|
>
|
||||||
|
{#if match.demo_parsed}
|
||||||
|
<CheckCircle2 class="h-2.5 w-2.5 text-neon-green" aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<Clock class="h-2.5 w-2.5 animate-pulse text-neon-gold" aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if match.demo_parsed}
|
|
||||||
<Badge variant="success" size="sm">Parsed</Badge>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Match Info -->
|
<!-- Match Info - Compact -->
|
||||||
<div class="p-4">
|
<div class="flex items-center justify-between px-3 py-2">
|
||||||
<!-- Score -->
|
<!-- Score -->
|
||||||
<div class="mb-3 flex items-center justify-center gap-3">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-mono text-2xl font-bold text-terrorist">{match.score_team_a}</span>
|
<span
|
||||||
<span class="text-base-content/40">-</span>
|
class="font-mono text-lg font-bold text-terrorist"
|
||||||
<span class="font-mono text-2xl font-bold text-ct">{match.score_team_b}</span>
|
style="text-shadow: 0 0 8px rgba(212, 167, 74, 0.4);"
|
||||||
|
>
|
||||||
|
{match.score_team_a}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-white/30">-</span>
|
||||||
|
<span
|
||||||
|
class="font-mono text-lg font-bold text-ct"
|
||||||
|
style="text-shadow: 0 0 8px rgba(94, 152, 217, 0.4);"
|
||||||
|
>
|
||||||
|
{match.score_team_b}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Meta -->
|
<!-- Meta -->
|
||||||
<div class="flex items-center justify-between text-sm text-base-content/60">
|
<div class="flex items-center gap-3 text-xs text-white/50">
|
||||||
<span>{formattedDate}</span>
|
<span>{formattedDate}</span>
|
||||||
{#if match.duration}
|
{#if match.duration}
|
||||||
<span>{Math.floor(match.duration / 60)}m</span>
|
<span>{Math.floor(match.duration / 60)}m</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Result Badge (inferred from score) -->
|
|
||||||
<div class="mt-3 flex justify-center">
|
|
||||||
{#if match.score_team_a === match.score_team_b}
|
|
||||||
<Badge variant="warning" size="sm">Tie</Badge>
|
|
||||||
{:else if match.score_team_a > match.score_team_b}
|
|
||||||
<Badge variant="success" size="sm">Team A Win</Badge>
|
|
||||||
{:else}
|
|
||||||
<Badge variant="error" size="sm">Team B Win</Badge>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
35
src/lib/components/match/MatchCardSkeleton.svelte
Normal file
35
src/lib/components/match/MatchCardSkeleton.svelte
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// Compact skeleton loading component for MatchCard
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<div
|
||||||
|
class="overflow-hidden rounded-lg border border-l-4 border-white/10 border-l-white/20 bg-void-light"
|
||||||
|
>
|
||||||
|
<!-- Map Header Skeleton - Compact -->
|
||||||
|
<div class="relative h-20 overflow-hidden bg-void-light">
|
||||||
|
<div class="absolute inset-0 animate-pulse bg-white/5"></div>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-t from-void via-void/70 to-transparent"></div>
|
||||||
|
<div class="relative flex h-full items-end justify-between p-2">
|
||||||
|
<div class="h-4 w-20 animate-pulse rounded bg-white/15"></div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="h-4 w-8 animate-pulse rounded-full bg-white/10"></div>
|
||||||
|
<div class="h-4 w-4 animate-pulse rounded-full bg-white/10"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Match Info Skeleton - Compact -->
|
||||||
|
<div class="flex items-center justify-between px-3 py-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="h-5 w-6 animate-pulse rounded bg-white/10"></div>
|
||||||
|
<span class="text-white/20">-</span>
|
||||||
|
<div class="h-5 w-6 animate-pulse rounded bg-white/10"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="h-3 w-16 animate-pulse rounded bg-white/10"></div>
|
||||||
|
<div class="h-3 w-8 animate-pulse rounded bg-white/10"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Upload, Check, AlertCircle, Loader2 } from 'lucide-svelte';
|
import { Upload, Check, AlertCircle, Loader2, ChevronDown } from 'lucide-svelte';
|
||||||
import { matchesAPI } from '$lib/api/matches';
|
import { matchesAPI } from '$lib/api/matches';
|
||||||
import { toast } from '$lib/stores/toast';
|
import { toast } from '$lib/stores/toast';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
@@ -9,10 +9,10 @@
|
|||||||
let parseStatus: 'idle' | 'parsing' | 'success' | 'error' = $state('idle');
|
let parseStatus: 'idle' | 'parsing' | 'success' | 'error' = $state('idle');
|
||||||
let statusMessage = $state('');
|
let statusMessage = $state('');
|
||||||
let parsedMatchId = $state('');
|
let parsedMatchId = $state('');
|
||||||
|
let showHelp = $state(false);
|
||||||
|
|
||||||
// Validate share code format
|
// Validate share code format
|
||||||
function isValidShareCode(code: string): boolean {
|
function isValidShareCode(code: string): boolean {
|
||||||
// Format: CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
|
|
||||||
const pattern = /^CSGO-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/;
|
const pattern = /^CSGO-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/;
|
||||||
return pattern.test(code.toUpperCase());
|
return pattern.test(code.toUpperCase());
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,6 @@
|
|||||||
'Match submitted successfully! Parsing may take a few minutes. You can view the match once parsing is complete.';
|
'Match submitted successfully! Parsing may take a few minutes. You can view the match once parsing is complete.';
|
||||||
toast.success('Match submitted for parsing!');
|
toast.success('Match submitted for parsing!');
|
||||||
|
|
||||||
// Wait a moment then redirect to the match page
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
goto(`/match/${response.match_id}`);
|
goto(`/match/${response.match_id}`);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -75,22 +74,22 @@
|
|||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Input Section -->
|
<!-- Input Section -->
|
||||||
<div class="form-control">
|
<div>
|
||||||
<label class="label" for="shareCode">
|
<label class="mb-2 block text-sm font-medium text-white" for="shareCode">
|
||||||
<span class="label-text font-medium">Submit Match Share Code</span>
|
Submit Match Share Code
|
||||||
</label>
|
</label>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-3">
|
||||||
<input
|
<input
|
||||||
id="shareCode"
|
id="shareCode"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
|
placeholder="CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
|
||||||
class="input input-bordered flex-1"
|
class="flex-1 rounded-lg border border-neon-blue/30 bg-void px-4 py-3 font-mono text-white transition-colors placeholder:text-white/40 focus:border-neon-blue focus:outline-none focus:ring-1 focus:ring-neon-blue disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
bind:value={shareCode}
|
bind:value={shareCode}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onkeydown={(e) => e.key === 'Enter' && handleSubmit()}
|
onkeydown={(e) => e.key === 'Enter' && handleSubmit()}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="flex items-center gap-2 rounded-lg bg-neon-blue px-6 py-3 font-semibold text-void transition-all hover:scale-105 hover:shadow-[0_0_20px_rgba(0,212,255,0.4)] focus:outline-none focus-visible:ring-2 focus-visible:ring-neon-blue focus-visible:ring-offset-2 focus-visible:ring-offset-void disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100"
|
||||||
onclick={handleSubmit}
|
onclick={handleSubmit}
|
||||||
disabled={isLoading || !shareCode.trim()}
|
disabled={isLoading || !shareCode.trim()}
|
||||||
>
|
>
|
||||||
@@ -102,54 +101,78 @@
|
|||||||
Parse
|
Parse
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="label">
|
<p class="mt-2 text-sm text-white/50">
|
||||||
<span class="label-text-alt text-base-content/60">
|
Submit a CS2 match share code to add it to the database
|
||||||
Submit a CS2 match share code to add it to the database
|
</p>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Messages -->
|
<!-- Status Messages -->
|
||||||
{#if parseStatus !== 'idle'}
|
{#if parseStatus !== 'idle'}
|
||||||
<div
|
<div
|
||||||
class="alert {parseStatus === 'success'
|
class="flex items-start gap-3 rounded-lg border p-4 {parseStatus === 'success'
|
||||||
? 'alert-success'
|
? 'border-neon-green/30 bg-neon-green/10'
|
||||||
: parseStatus === 'error'
|
: parseStatus === 'error'
|
||||||
? 'alert-error'
|
? 'border-neon-red/30 bg-neon-red/10'
|
||||||
: 'alert-info'}"
|
: 'border-neon-blue/30 bg-neon-blue/10'}"
|
||||||
>
|
>
|
||||||
{#if parseStatus === 'parsing'}
|
{#if parseStatus === 'parsing'}
|
||||||
<Loader2 class="h-6 w-6 shrink-0 animate-spin stroke-current" />
|
<Loader2 class="h-5 w-5 shrink-0 animate-spin text-neon-blue" />
|
||||||
{:else if parseStatus === 'success'}
|
{:else if parseStatus === 'success'}
|
||||||
<Check class="h-6 w-6 shrink-0 stroke-current" />
|
<Check class="h-5 w-5 shrink-0 text-neon-green" />
|
||||||
{:else}
|
{:else}
|
||||||
<AlertCircle class="h-6 w-6 shrink-0 stroke-current" />
|
<AlertCircle class="h-5 w-5 shrink-0 text-neon-red" />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p>{statusMessage}</p>
|
<p
|
||||||
|
class={parseStatus === 'success'
|
||||||
|
? 'text-neon-green'
|
||||||
|
: parseStatus === 'error'
|
||||||
|
? 'text-neon-red'
|
||||||
|
: 'text-neon-blue'}
|
||||||
|
>
|
||||||
|
{statusMessage}
|
||||||
|
</p>
|
||||||
{#if parseStatus === 'success' && parsedMatchId}
|
{#if parseStatus === 'success' && parsedMatchId}
|
||||||
<p class="mt-1 text-sm">Redirecting to match page...</p>
|
<p class="mt-1 text-sm text-white/50">Redirecting to match page...</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if parseStatus !== 'parsing'}
|
{#if parseStatus !== 'parsing'}
|
||||||
<button class="btn btn-ghost btn-sm" onclick={resetForm}>Dismiss</button>
|
<button
|
||||||
|
class="rounded px-2 py-1 text-sm text-white/50 transition-colors hover:bg-white/10 hover:text-white"
|
||||||
|
onclick={resetForm}
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Help Text -->
|
<!-- Help Text (Collapsible) -->
|
||||||
<div class="text-sm text-base-content/70">
|
<div class="rounded-lg border border-white/10 bg-void">
|
||||||
<p class="mb-2 font-medium">How to get your match share code:</p>
|
<button
|
||||||
<ol class="list-inside list-decimal space-y-1">
|
type="button"
|
||||||
<li>Open CS2 and navigate to your Matches tab</li>
|
class="flex w-full items-center justify-between px-4 py-3 text-left text-sm font-medium text-white/70 transition-colors hover:text-white"
|
||||||
<li>Click on a match you want to analyze</li>
|
onclick={() => (showHelp = !showHelp)}
|
||||||
<li>Click the "Copy Share Link" button</li>
|
>
|
||||||
<li>Paste the share code here</li>
|
<span>How to get your match share code</span>
|
||||||
</ol>
|
<ChevronDown
|
||||||
<p class="mt-2 text-xs">
|
class="h-4 w-4 transition-transform duration-200 {showHelp ? 'rotate-180' : ''}"
|
||||||
Note: Demo parsing can take 1-5 minutes depending on match length. You'll be able to view
|
/>
|
||||||
basic match info immediately, but detailed statistics will be available after parsing
|
</button>
|
||||||
completes.
|
{#if showHelp}
|
||||||
</p>
|
<div class="border-t border-white/10 px-4 py-3">
|
||||||
|
<ol class="list-inside list-decimal space-y-2 text-sm text-white/60">
|
||||||
|
<li>Open CS2 and navigate to your Matches tab</li>
|
||||||
|
<li>Click on a match you want to analyze</li>
|
||||||
|
<li>Click the "Copy Share Link" button</li>
|
||||||
|
<li>Paste the share code here</li>
|
||||||
|
</ol>
|
||||||
|
<p class="mt-3 text-xs text-white/40">
|
||||||
|
Note: Demo parsing can take 1-5 minutes depending on match length. You'll be able to view
|
||||||
|
basic match info immediately, but detailed statistics will be available after parsing
|
||||||
|
completes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
children: Snippet;
|
children: Snippet;
|
||||||
onclick?: () => void;
|
onclick?: () => void;
|
||||||
class?: string;
|
class?: string;
|
||||||
|
external?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -16,7 +17,8 @@
|
|||||||
size = 'md',
|
size = 'md',
|
||||||
children,
|
children,
|
||||||
onclick,
|
onclick,
|
||||||
class: className = ''
|
class: className = '',
|
||||||
|
external = false
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
@@ -60,6 +62,7 @@
|
|||||||
<a
|
<a
|
||||||
{href}
|
{href}
|
||||||
class="inline-flex items-center justify-center rounded-lg font-semibold transition-all duration-300 hover:scale-105 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-void motion-reduce:transition-none motion-reduce:hover:scale-100 {classes.bg} {classes.text} {classes.glow} {sizeClass} {className}"
|
class="inline-flex items-center justify-center rounded-lg font-semibold transition-all duration-300 hover:scale-105 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-void motion-reduce:transition-none motion-reduce:hover:scale-100 {classes.bg} {classes.text} {classes.glow} {sizeClass} {className}"
|
||||||
|
{...external ? { target: '_blank', rel: 'noopener noreferrer' } : {}}
|
||||||
>
|
>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -5,24 +5,41 @@
|
|||||||
|
|
||||||
const STORAGE_KEY = 'matches-navigation-state';
|
const STORAGE_KEY = 'matches-navigation-state';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter state for matches page - used for preserving filters across navigation
|
||||||
|
*/
|
||||||
|
export interface FilterState {
|
||||||
|
resultFilter: 'all' | 'win' | 'loss' | 'tie';
|
||||||
|
sortBy: 'date' | 'duration' | 'score';
|
||||||
|
sortOrder: 'desc' | 'asc';
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface NavigationState {
|
interface NavigationState {
|
||||||
matchId: string;
|
matchId: string;
|
||||||
scrollY: number;
|
scrollY: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
loadedCount: number; // Number of matches loaded (for pagination)
|
loadedCount: number; // Number of matches loaded (for pagination)
|
||||||
|
filters?: FilterState; // Optional filter state
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store navigation state when leaving the matches page
|
* Store navigation state when leaving the matches page
|
||||||
*/
|
*/
|
||||||
export function storeMatchesState(matchId: string, loadedCount: number): void {
|
export function storeMatchesState(
|
||||||
|
matchId: string,
|
||||||
|
loadedCount: number,
|
||||||
|
filters?: FilterState
|
||||||
|
): void {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
const state: NavigationState = {
|
const state: NavigationState = {
|
||||||
matchId,
|
matchId,
|
||||||
scrollY: window.scrollY,
|
scrollY: window.scrollY,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
loadedCount
|
loadedCount,
|
||||||
|
filters
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user