forked from CSGOWTF/csgowtf
This commit completes the first phase of feature parity implementation and resolves all API integration issues to match the backend API format. ## API Integration Fixes - Remove all hardcoded default values from transformers (tick_rate, kast, player_count, steam_updated) - Update TypeScript types to make fields optional where backend doesn't guarantee them - Update Zod schemas to validate optional fields correctly - Fix mock data to match real API response format (plain arrays, not wrapped objects) - Update UI components to handle undefined values with proper fallbacks - Add comprehensive API documentation for Match and Player endpoints ## Phase 1 Features Implemented (3/6) ### 1. Player Tracking System ✅ - Created TrackPlayerModal.svelte with auth code input - Integrated track/untrack player API endpoints - Added UI for providing optional share code - Displays tracked status on player profiles - Full validation and error handling ### 2. Share Code Parsing ✅ - Created ShareCodeInput.svelte component - Added to matches page for easy match submission - Real-time validation of share code format - Parse status feedback with loading states - Auto-redirect to match page on success ### 3. VAC/Game Ban Status ✅ - Added VAC and game ban count/date fields to Player type - Display status badges on player profile pages - Show ban count and date when available - Visual indicators using DaisyUI badge components ## Component Improvements - Modal.svelte: Added Svelte 5 Snippet types, actions slot support - ThemeToggle.svelte: Removed deprecated svelte:component usage - Tooltip.svelte: Fixed type safety with Snippet type - All new components follow Svelte 5 runes pattern ($state, $derived, $bindable) ## Type Safety & Linting - Fixed all ESLint errors (any types → proper types) - Fixed form label accessibility issues - Replaced error: any with error: unknown + proper type guards - Added Snippet type imports where needed - Updated all catch blocks to use instanceof Error checks ## Static Assets - Migrated all files from public/ to static/ directory per SvelteKit best practices - Moved 200+ map icons, screenshots, and other assets - Updated all import paths to use /images/ (served from static/) ## Documentation - Created IMPLEMENTATION_STATUS.md tracking all 15 missing features - Updated API.md with optional field annotations - Created MATCHES_API.md with comprehensive endpoint documentation - Added inline comments marking optional vs required fields ## Testing - Updated mock fixtures to remove default values - Fixed mock handlers to return plain arrays like real API - Ensured all components handle undefined gracefully ## Remaining Phase 1 Tasks - [ ] Add VAC status column to match scoreboard - [ ] Create weapons statistics tab for matches - [ ] Implement recently visited players on home page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
292 lines
9.9 KiB
Svelte
292 lines
9.9 KiB
Svelte
<script lang="ts">
|
|
import { Search, TrendingUp, Users, Zap, ChevronLeft, ChevronRight } from 'lucide-svelte';
|
|
import Button from '$lib/components/ui/Button.svelte';
|
|
import Card from '$lib/components/ui/Card.svelte';
|
|
import Badge from '$lib/components/ui/Badge.svelte';
|
|
import MatchCard from '$lib/components/match/MatchCard.svelte';
|
|
import type { PageData } from './$types';
|
|
|
|
// Get data from page loader
|
|
let { data }: { data: PageData } = $props();
|
|
|
|
// Use matches directly - already transformed by API client
|
|
const featuredMatches = data.featuredMatches;
|
|
|
|
const stats = [
|
|
{ icon: Users, label: 'Players Tracked', value: '1.2M+' },
|
|
{ icon: TrendingUp, label: 'Matches Analyzed', value: '500K+' },
|
|
{ icon: Zap, label: 'Demos Parsed', value: '2M+' }
|
|
];
|
|
|
|
// Carousel state
|
|
let currentSlide = $state(0);
|
|
let isPaused = $state(false);
|
|
let autoRotateInterval: ReturnType<typeof setInterval> | null = null;
|
|
let manualNavigationTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
let windowWidth = $state(1024); // Default to desktop
|
|
|
|
// Track window width for responsive slides
|
|
$effect(() => {
|
|
if (typeof window !== 'undefined') {
|
|
windowWidth = window.innerWidth;
|
|
|
|
const handleResize = () => {
|
|
windowWidth = window.innerWidth;
|
|
};
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
return () => {
|
|
window.removeEventListener('resize', handleResize);
|
|
};
|
|
}
|
|
// Return empty cleanup function for server-side rendering path
|
|
return () => {};
|
|
});
|
|
|
|
// Determine matches per slide based on screen width
|
|
const matchesPerSlide = $derived(windowWidth < 768 ? 1 : windowWidth < 1024 ? 2 : 3);
|
|
|
|
const totalSlides = $derived(Math.ceil(featuredMatches.length / matchesPerSlide));
|
|
|
|
// Get visible matches for current slide
|
|
const visibleMatches = $derived.by(() => {
|
|
const start = currentSlide * matchesPerSlide;
|
|
return featuredMatches.slice(start, start + matchesPerSlide);
|
|
});
|
|
|
|
function nextSlide() {
|
|
currentSlide = (currentSlide + 1) % totalSlides;
|
|
}
|
|
|
|
function prevSlide() {
|
|
currentSlide = (currentSlide - 1 + totalSlides) % totalSlides;
|
|
}
|
|
|
|
function goToSlide(index: number) {
|
|
currentSlide = index;
|
|
pauseAutoRotateTemporarily();
|
|
}
|
|
|
|
function pauseAutoRotateTemporarily() {
|
|
isPaused = true;
|
|
if (manualNavigationTimeout) clearTimeout(manualNavigationTimeout);
|
|
manualNavigationTimeout = setTimeout(() => {
|
|
isPaused = false;
|
|
}, 10000); // Resume after 10 seconds
|
|
}
|
|
|
|
function handleManualNavigation(direction: 'prev' | 'next') {
|
|
if (direction === 'prev') {
|
|
prevSlide();
|
|
} else {
|
|
nextSlide();
|
|
}
|
|
pauseAutoRotateTemporarily();
|
|
}
|
|
|
|
// Auto-rotation effect
|
|
$effect(() => {
|
|
if (autoRotateInterval) clearInterval(autoRotateInterval);
|
|
|
|
autoRotateInterval = setInterval(() => {
|
|
if (!isPaused) {
|
|
nextSlide();
|
|
}
|
|
}, 5000);
|
|
|
|
return () => {
|
|
if (autoRotateInterval) clearInterval(autoRotateInterval);
|
|
if (manualNavigationTimeout) clearTimeout(manualNavigationTimeout);
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>{data.meta.title}</title>
|
|
<meta name="description" content={data.meta.description} />
|
|
</svelte:head>
|
|
|
|
<!-- Hero Section -->
|
|
<section class="border-b border-base-300 bg-gradient-to-b from-base-100 to-base-200 py-24">
|
|
<div class="container mx-auto px-4">
|
|
<div class="mx-auto max-w-4xl text-center">
|
|
<div class="mb-6">
|
|
<Badge variant="info" size="md">🎮 Now supporting CS2</Badge>
|
|
</div>
|
|
|
|
<h1 class="mb-6 text-6xl font-bold leading-tight md:text-7xl">
|
|
<span class="text-primary">CS2</span><span class="text-secondary">.WTF</span>
|
|
</h1>
|
|
|
|
<p class="mb-8 text-xl text-base-content/70 md:text-2xl">
|
|
Track your performance, analyze matches, and improve your game with
|
|
<span class="font-semibold text-primary">detailed statistics</span> and insights.
|
|
</p>
|
|
|
|
<div class="mb-12 flex flex-col justify-center gap-4 sm:flex-row">
|
|
<Button variant="primary" size="lg" href="/matches">
|
|
<Search class="mr-2 h-5 w-5" />
|
|
Browse Matches
|
|
</Button>
|
|
<Button variant="secondary" size="lg" href="/player/76561198012345678">
|
|
<Users class="mr-2 h-5 w-5" />
|
|
View Demo Profile
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Stats Grid -->
|
|
<div class="grid gap-6 md:grid-cols-3">
|
|
{#each stats as stat}
|
|
{@const StatIcon = stat.icon}
|
|
<div class="rounded-lg bg-base-100 p-6 shadow-lg">
|
|
<StatIcon class="mx-auto mb-3 h-8 w-8 text-primary" />
|
|
<div class="text-3xl font-bold text-base-content">{stat.value}</div>
|
|
<div class="text-sm text-base-content/60">{stat.label}</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Featured Matches -->
|
|
<section class="py-16">
|
|
<div class="container mx-auto px-4">
|
|
<div class="mb-8 flex items-center justify-between">
|
|
<div>
|
|
<h2 class="text-3xl font-bold text-base-content">Featured Matches</h2>
|
|
<p class="mt-2 text-base-content/60">Latest competitive matches from our community</p>
|
|
</div>
|
|
<Button variant="ghost" href="/matches">View All</Button>
|
|
</div>
|
|
|
|
{#if featuredMatches.length > 0}
|
|
<!-- Carousel Container -->
|
|
<div
|
|
class="relative"
|
|
onmouseenter={() => (isPaused = true)}
|
|
onmouseleave={() => (isPaused = false)}
|
|
role="region"
|
|
aria-label="Featured matches carousel"
|
|
>
|
|
<!-- Matches Grid with Fade Transition -->
|
|
<div class="transition-opacity duration-500" class:opacity-100={true}>
|
|
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
{#each visibleMatches as match (match.match_id)}
|
|
<MatchCard {match} />
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation Arrows - Only show if there are multiple slides -->
|
|
{#if totalSlides > 1}
|
|
<!-- Previous Button -->
|
|
<button
|
|
onclick={() => handleManualNavigation('prev')}
|
|
class="group absolute left-0 top-1/2 z-10 -translate-x-4 -translate-y-1/2 rounded-md border border-base-content/10 bg-base-100/95 p-2 shadow-[0_8px_30px_rgb(0,0,0,0.12)] backdrop-blur-md transition-all duration-200 hover:-translate-x-5 hover:border-primary/30 hover:bg-base-100 hover:shadow-[0_12px_40px_rgb(0,0,0,0.15)] focus:outline-none focus:ring-2 focus:ring-primary/50 md:-translate-x-6 md:hover:-translate-x-7"
|
|
aria-label="Previous slide"
|
|
>
|
|
<ChevronLeft
|
|
class="h-6 w-6 text-base-content/70 transition-colors duration-200 group-hover:text-primary"
|
|
/>
|
|
</button>
|
|
|
|
<!-- Next Button -->
|
|
<button
|
|
onclick={() => handleManualNavigation('next')}
|
|
class="group absolute right-0 top-1/2 z-10 -translate-y-1/2 translate-x-4 rounded-md border border-base-content/10 bg-base-100/95 p-2 shadow-[0_8px_30px_rgb(0,0,0,0.12)] backdrop-blur-md transition-all duration-200 hover:translate-x-5 hover:border-primary/30 hover:bg-base-100 hover:shadow-[0_12px_40px_rgb(0,0,0,0.15)] focus:outline-none focus:ring-2 focus:ring-primary/50 md:translate-x-6 md:hover:translate-x-7"
|
|
aria-label="Next slide"
|
|
>
|
|
<ChevronRight
|
|
class="h-6 w-6 text-base-content/70 transition-colors duration-200 group-hover:text-primary"
|
|
/>
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Dot Indicators - Only show if there are multiple slides -->
|
|
{#if totalSlides > 1}
|
|
<div class="mt-8 flex justify-center gap-2">
|
|
{#each Array(totalSlides) as _, i}
|
|
<button
|
|
onclick={() => goToSlide(i)}
|
|
class="h-2 w-2 rounded-full transition-all duration-300 hover:scale-125 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
|
class:bg-primary={i === currentSlide}
|
|
class:w-8={i === currentSlide}
|
|
class:bg-base-300={i !== currentSlide}
|
|
aria-label={`Go to slide ${i + 1}`}
|
|
></button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{:else}
|
|
<!-- No Matches Found -->
|
|
<div class="rounded-lg border border-base-300 bg-base-100 p-12 text-center">
|
|
<p class="text-lg text-base-content/60">No featured matches available at the moment.</p>
|
|
<p class="mt-2 text-sm text-base-content/40">Check back soon for the latest matches!</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Features Section -->
|
|
<section class="border-t border-base-300 bg-base-200 py-16">
|
|
<div class="container mx-auto px-4">
|
|
<div class="mb-12 text-center">
|
|
<h2 class="text-3xl font-bold text-base-content">Why CS2.WTF?</h2>
|
|
<p class="mt-2 text-base-content/60">Everything you need to analyze your CS2 performance</p>
|
|
</div>
|
|
|
|
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
<Card padding="lg">
|
|
<div class="mb-4 inline-flex rounded-lg bg-primary/10 p-3">
|
|
<TrendingUp class="h-6 w-6 text-primary" />
|
|
</div>
|
|
<h3 class="mb-2 text-xl font-semibold">Detailed Statistics</h3>
|
|
<p class="text-base-content/60">
|
|
Track K/D, ADR, HS%, KAST, and more. Analyze your performance round-by-round with
|
|
comprehensive stats.
|
|
</p>
|
|
</Card>
|
|
|
|
<Card padding="lg">
|
|
<div class="mb-4 inline-flex rounded-lg bg-secondary/10 p-3">
|
|
<Zap class="h-6 w-6 text-secondary" />
|
|
</div>
|
|
<h3 class="mb-2 text-xl font-semibold">Economy Tracking</h3>
|
|
<p class="text-base-content/60">
|
|
Understand money management with round-by-round economy analysis and spending patterns.
|
|
</p>
|
|
</Card>
|
|
|
|
<Card padding="lg">
|
|
<div class="mb-4 inline-flex rounded-lg bg-info/10 p-3">
|
|
<Users class="h-6 w-6 text-info" />
|
|
</div>
|
|
<h3 class="mb-2 text-xl font-semibold">Player Profiles</h3>
|
|
<p class="text-base-content/60">
|
|
View comprehensive player profiles with match history, favorite maps, and performance
|
|
trends.
|
|
</p>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- CTA Section -->
|
|
<section class="py-16">
|
|
<div class="container mx-auto px-4">
|
|
<Card variant="elevated" padding="lg">
|
|
<div class="text-center">
|
|
<h2 class="mb-4 text-3xl font-bold text-base-content">Ready to improve your game?</h2>
|
|
<p class="mb-8 text-lg text-base-content/70">
|
|
Start tracking your CS2 matches and get insights that help you rank up.
|
|
</p>
|
|
<Button variant="primary" size="lg" href="/matches">Get Started - It's Free</Button>
|
|
<p class="mt-4 text-sm text-base-content/50">Free and open source. No signup required.</p>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</section>
|