Files
csgowtf/src/routes/+page.svelte
vikingowl 8f3b652740 feat: Implement Phase 1 critical features and fix API integration
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>
2025-11-12 19:31:18 +01:00

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>