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>
This commit is contained in:
2025-11-12 19:31:18 +01:00
parent a861b1c1b6
commit 8f3b652740
422 changed files with 106174 additions and 102193 deletions

196
src/lib/utils/formatters.ts Normal file
View File

@@ -0,0 +1,196 @@
/**
* Formatting utilities for CS2 data
*/
/**
* Premier rating tier information
*/
export interface PremierRatingTier {
/** Formatted rating with comma separator (e.g., "15,000") */
formatted: string;
/** Hex color for this tier */
color: string;
/** Tier name */
tier: string;
/** Tailwind CSS classes for styling */
cssClasses: string;
}
/**
* Format Premier rating and return tier information
* CS2 Premier rating range: 0-30000
* Color tiers: <5000 (gray), 5000-9999 (blue), 10000-14999 (purple),
* 15000-19999 (pink), 20000-24999 (red), 25000+ (gold)
*
* @param rating - Premier rating (0-30000)
* @returns Tier information with formatted rating and colors
*/
export function formatPremierRating(rating: number | undefined | null): PremierRatingTier {
// Default for unranked/unknown
if (rating === undefined || rating === null || rating === 0) {
return {
formatted: 'Unranked',
color: '#9CA3AF',
tier: 'Unranked',
cssClasses: 'bg-base-300/50 border-base-content/20 text-base-content/60'
};
}
// Ensure rating is within valid range
const validRating = Math.max(0, Math.min(30000, rating));
const formatted = validRating.toLocaleString('en-US');
// Determine tier based on rating
if (validRating >= 25000) {
return {
formatted,
color: '#EAB308',
tier: 'Legendary',
cssClasses:
'bg-gradient-to-br from-yellow-500/20 to-amber-600/20 border-yellow-500/40 text-yellow-400 font-bold shadow-lg shadow-yellow-500/20'
};
} else if (validRating >= 20000) {
return {
formatted,
color: '#EF4444',
tier: 'Elite',
cssClasses:
'bg-gradient-to-br from-red-500/20 to-rose-600/20 border-red-500/40 text-red-400 font-semibold shadow-md shadow-red-500/10'
};
} else if (validRating >= 15000) {
return {
formatted,
color: '#EC4899',
tier: 'Expert',
cssClasses:
'bg-gradient-to-br from-pink-500/20 to-fuchsia-500/20 border-pink-500/40 text-pink-400 font-semibold shadow-md shadow-pink-500/10'
};
} else if (validRating >= 10000) {
return {
formatted,
color: '#A855F7',
tier: 'Advanced',
cssClasses:
'bg-gradient-to-br from-purple-500/20 to-violet-600/20 border-purple-500/40 text-purple-400 font-medium'
};
} else if (validRating >= 5000) {
return {
formatted,
color: '#3B82F6',
tier: 'Intermediate',
cssClasses:
'bg-gradient-to-br from-blue-500/20 to-indigo-500/20 border-blue-500/40 text-blue-400'
};
} else {
return {
formatted,
color: '#9CA3AF',
tier: 'Beginner',
cssClasses: 'bg-gray-500/10 border-gray-500/30 text-gray-400'
};
}
}
/**
* Get Tailwind CSS classes for Premier rating badge
* @param rating - Premier rating (0-30000)
* @returns Tailwind CSS class string
*/
export function getPremierRatingClass(rating: number | undefined | null): string {
return formatPremierRating(rating).cssClasses;
}
/**
* Calculate rating change display
* @param oldRating - Previous rating
* @param newRating - New rating
* @returns Object with change amount and display string
*/
export function getPremierRatingChange(
oldRating: number | undefined | null,
newRating: number | undefined | null
): {
change: number;
display: string;
isPositive: boolean;
cssClasses: string;
} | null {
if (
oldRating === undefined ||
oldRating === null ||
newRating === undefined ||
newRating === null
) {
return null;
}
const change = newRating - oldRating;
if (change === 0) {
return {
change: 0,
display: '±0',
isPositive: false,
cssClasses: 'text-base-content/60'
};
}
const isPositive = change > 0;
const display = isPositive ? `+${change}` : change.toString();
return {
change,
display,
isPositive,
cssClasses: isPositive ? 'text-success font-semibold' : 'text-error font-semibold'
};
}
/**
* Format K/D ratio
* @param kills - Number of kills
* @param deaths - Number of deaths
* @returns Formatted K/D ratio
*/
export function formatKD(kills: number, deaths: number): string {
if (deaths === 0) {
return kills.toFixed(2);
}
return (kills / deaths).toFixed(2);
}
/**
* Format percentage
* @param value - Percentage value (0-100)
* @param decimals - Number of decimal places (default: 1)
* @returns Formatted percentage string
*/
export function formatPercent(value: number | undefined | null, decimals = 1): string {
if (value === undefined || value === null) {
return '0.0%';
}
return `${value.toFixed(decimals)}%`;
}
/**
* Format duration in seconds to MM:SS
* @param seconds - Duration in seconds
* @returns Formatted duration string
*/
export function formatDuration(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
/**
* Format large numbers with comma separators
* @param value - Number to format
* @returns Formatted number string
*/
export function formatNumber(value: number | undefined | null): string {
if (value === undefined || value === null) {
return '0';
}
return value.toLocaleString('en-US');
}