feat: Add neon esports landing page with WCAG accessibility
- Create HeroSection with animated search bar and stat counters - Add LiveMatchTicker with auto-scrolling recent matches - Build FlashLeaderboard "Wall of Shame" with podium display - Implement FeatureShowcase with scroll-triggered animations - Add NeonCTA call-to-action section with trust badges - Create reusable NeonButton component with glow effects Accessibility improvements: - Add aria-labels, aria-hidden for decorative elements - Implement focus-visible ring styles for keyboard navigation - Support prefers-reduced-motion across all animations - Use semantic HTML (article, nav, dl) for screen readers - Improve color contrast ratios for WCAG compliance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
81
src/lib/components/landing/LiveMatchTicker.svelte
Normal file
81
src/lib/components/landing/LiveMatchTicker.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import LiveMatchTickerCard from './LiveMatchTickerCard.svelte';
|
||||
import { Activity } from 'lucide-svelte';
|
||||
|
||||
interface Match {
|
||||
id: string;
|
||||
map: string;
|
||||
scoreT: number;
|
||||
scoreCT: number;
|
||||
isProcessing?: boolean;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
matches?: Match[];
|
||||
}
|
||||
|
||||
// Sample matches for demo - in production, this would come from the API
|
||||
let {
|
||||
matches = [
|
||||
{ id: '1', map: 'de_dust2', scoreT: 16, scoreCT: 14, isProcessing: true },
|
||||
{ id: '2', map: 'de_mirage', scoreT: 13, scoreCT: 16 },
|
||||
{ id: '3', map: 'de_inferno', scoreT: 16, scoreCT: 9 },
|
||||
{ id: '4', map: 'de_ancient', scoreT: 11, scoreCT: 16 },
|
||||
{ id: '5', map: 'de_anubis', scoreT: 16, scoreCT: 12, isProcessing: true },
|
||||
{ id: '6', map: 'de_nuke', scoreT: 8, scoreCT: 16 },
|
||||
{ id: '7', map: 'de_overpass', scoreT: 16, scoreCT: 14 },
|
||||
{ id: '8', map: 'de_vertigo', scoreT: 14, scoreCT: 16 }
|
||||
]
|
||||
}: Props = $props();
|
||||
|
||||
// Duplicate matches for seamless loop with unique keys
|
||||
const duplicatedMatches = $derived([
|
||||
...matches.map((m, i) => ({ ...m, uniqueKey: `first-${i}-${m.id}` })),
|
||||
...matches.map((m, i) => ({ ...m, uniqueKey: `second-${i}-${m.id}` }))
|
||||
]);
|
||||
</script>
|
||||
|
||||
<section class="relative overflow-hidden bg-void py-8" aria-labelledby="recent-matches-heading">
|
||||
<!-- Section Header -->
|
||||
<div class="container mx-auto mb-6 flex items-center justify-between px-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<Activity
|
||||
class="h-5 w-5 animate-pulse text-neon-green motion-reduce:animate-none"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h2 id="recent-matches-heading" class="text-lg font-semibold text-white">Recent Matches</h2>
|
||||
</div>
|
||||
<a
|
||||
href="/matches"
|
||||
class="rounded text-sm text-neon-blue transition-colors hover:text-neon-blue/80 focus:outline-none focus:ring-2 focus:ring-neon-blue focus:ring-offset-2 focus:ring-offset-void"
|
||||
>
|
||||
View all →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Ticker Container -->
|
||||
<div class="relative">
|
||||
<!-- Left Fade -->
|
||||
<div
|
||||
class="pointer-events-none absolute left-0 top-0 z-10 h-full w-24 bg-gradient-to-r from-void to-transparent"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<!-- Right Fade -->
|
||||
<div
|
||||
class="pointer-events-none absolute right-0 top-0 z-10 h-full w-24 bg-gradient-to-l from-void to-transparent"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<!-- Scrolling Ticker -->
|
||||
<nav
|
||||
class="hover:pause-animation flex animate-ticker gap-4 motion-reduce:animate-none"
|
||||
aria-label="Recent match scores"
|
||||
>
|
||||
{#each duplicatedMatches as match (match.uniqueKey)}
|
||||
<LiveMatchTickerCard {match} />
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
Reference in New Issue
Block a user