Files
csgowtf/src/lib/utils/export.ts
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

155 lines
4.2 KiB
TypeScript

/**
* Export utilities for match data
* Provides CSV and JSON export functionality for match listings
*/
import type { MatchListItem } from '$lib/types';
import { formatDuration } from './formatters';
/**
* Format date to readable string (YYYY-MM-DD HH:MM)
* @param dateString - ISO date string
* @returns Formatted date string
*/
function formatDateForExport(dateString: string): string {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
/**
* Convert matches array to CSV format
* @param matches - Array of match items to export
* @returns CSV string
*/
function matchesToCSV(matches: MatchListItem[]): string {
// CSV Headers
const headers = [
'Match ID',
'Date',
'Map',
'Score Team A',
'Score Team B',
'Duration',
'Demo Parsed',
'Player Count'
];
// CSV rows
const rows = matches.map((match) => {
return [
match.match_id,
formatDateForExport(match.date),
match.map,
match.score_team_a.toString(),
match.score_team_b.toString(),
formatDuration(match.duration),
match.demo_parsed ? 'Yes' : 'No',
match.player_count?.toString() || '-'
];
});
// Combine headers and rows
const csvContent = [
headers.join(','),
...rows.map((row) =>
row
.map((cell) => {
// Escape cells containing commas or quotes
if (cell.includes(',') || cell.includes('"')) {
return `"${cell.replace(/"/g, '""')}"`;
}
return cell;
})
.join(',')
)
].join('\n');
return csvContent;
}
/**
* Convert matches array to formatted JSON
* @param matches - Array of match items to export
* @returns Formatted JSON string
*/
function matchesToJSON(matches: MatchListItem[]): string {
// Create clean export format
const exportData = {
export_date: new Date().toISOString(),
total_matches: matches.length,
matches: matches.map((match) => ({
match_id: match.match_id,
date: formatDateForExport(match.date),
map: match.map,
score: `${match.score_team_a} - ${match.score_team_b}`,
score_team_a: match.score_team_a,
score_team_b: match.score_team_b,
duration: formatDuration(match.duration),
duration_seconds: match.duration,
demo_parsed: match.demo_parsed,
player_count: match.player_count
}))
};
return JSON.stringify(exportData, null, 2);
}
/**
* Trigger browser download for a file
* @param content - File content
* @param filename - Name of file to download
* @param mimeType - MIME type of file
*/
function triggerDownload(content: string, filename: string, mimeType: string): void {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* Export matches to CSV file
* Generates and downloads a CSV file with match data
* @param matches - Array of match items to export
* @throws Error if matches array is empty
*/
export function exportMatchesToCSV(matches: MatchListItem[]): void {
if (!matches || matches.length === 0) {
throw new Error('No matches to export');
}
const csvContent = matchesToCSV(matches);
const timestamp = new Date().toISOString().split('T')[0];
const filename = `cs2wtf-matches-${timestamp}.csv`;
triggerDownload(csvContent, filename, 'text/csv;charset=utf-8;');
}
/**
* Export matches to JSON file
* Generates and downloads a JSON file with match data
* @param matches - Array of match items to export
* @throws Error if matches array is empty
*/
export function exportMatchesToJSON(matches: MatchListItem[]): void {
if (!matches || matches.length === 0) {
throw new Error('No matches to export');
}
const jsonContent = matchesToJSON(matches);
const timestamp = new Date().toISOString().split('T')[0];
const filename = `cs2wtf-matches-${timestamp}.json`;
triggerDownload(jsonContent, filename, 'application/json;charset=utf-8;');
}