forked from CSGOWTF/csgowtf
feat: CS2 format support, player tracking fixes, and homepage enhancements
- Add dynamic MR12/MR15 halftime calculation to RoundTimeline component - Fix TrackPlayerModal API signature (remove shareCode, change isOpen to open binding) - Update Player types for string IDs and add ban fields (vac_count, vac_date, game_ban_count, game_ban_date) - Add target/rel props to Button component for external links - Enhance homepage with processing matches indicator - Update preferences store for string player IDs - Document Phase 5 progress and TypeScript fixes in TODO.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
484
TODO.md
484
TODO.md
@@ -190,40 +190,55 @@
|
|||||||
|
|
||||||
## Phase 5 – Feature Delivery (Parity + Enhancements)
|
## Phase 5 – Feature Delivery (Parity + Enhancements)
|
||||||
|
|
||||||
### 5.1 Homepage (`/` - `src/routes/+page.svelte`)
|
### 5.1 Homepage (`/` - `src/routes/+page.svelte`) ✅ SUBSTANTIALLY COMPLETE
|
||||||
|
|
||||||
- [ ] Hero section with site branding and tagline
|
- [x] Hero section with site branding and tagline
|
||||||
- [ ] Featured/Recent matches carousel:
|
- [x] Featured/Recent matches carousel:
|
||||||
- Card component showing map, teams, score, date
|
- ✅ Card component showing map, teams, score, date
|
||||||
- Click to navigate to match detail
|
- ✅ Click to navigate to match detail
|
||||||
- Auto-rotate with pause on hover
|
- ✅ Auto-rotate with pause on hover (5 second intervals)
|
||||||
- [ ] Live matches indicator (if backend supports):
|
- ✅ Manual navigation arrows with temporary pause
|
||||||
- Real-time badge/pulse animation
|
- ✅ Responsive slides (1/2/3 matches based on screen width)
|
||||||
- Current round score updates
|
- [x] Processing matches indicator:
|
||||||
- [ ] Quick stats dashboard:
|
- ✅ Badge with pulsing animation for unparsed demos
|
||||||
- Total matches analyzed
|
- ✅ Count of matches being processed
|
||||||
- Most played maps (pie chart)
|
- [x] Quick stats dashboard:
|
||||||
- Top performers this week/month
|
- ✅ Total matches analyzed (dynamic from last 50 matches)
|
||||||
- [ ] Search bar prominently placed (autocomplete for players/matches)
|
- ✅ Most played maps (pie chart with top 7 maps)
|
||||||
- [ ] Call-to-action: "Upload Your Demo" or "Search Matches"
|
- ✅ Recent activity stats cards
|
||||||
|
- ⚠️ Top performers this week/month - **Deferred** (would require 50+ API calls for player details)
|
||||||
|
- [x] Search bar in header (global navigation)
|
||||||
|
- [x] Call-to-action sections ("Why CS2.WTF?", "Ready to improve?")
|
||||||
|
- [x] Recently visited players component
|
||||||
|
|
||||||
### 5.2 Matches Listing (`/matches` - `src/routes/matches/+page.svelte`)
|
### 5.2 Matches Listing (`/matches` - `src/routes/matches/+page.svelte`) ✅ COMPLETE (with documented blockers)
|
||||||
|
|
||||||
- [ ] Advanced filter panel (collapsible on mobile):
|
- [x] Advanced filter panel (collapsible on mobile):
|
||||||
- Date range picker (from/to with presets: today, week, month)
|
- [x] Date range picker (from/to with presets: today, week, month)
|
||||||
- Map selector (multi-select with map thumbnails)
|
- [x] Map selector (multi-select with map thumbnails)
|
||||||
- Rank tier filter (CS2 Premier rating ranges)
|
- [x] Result filter (win/loss/tie)
|
||||||
- Game mode filter (Premier, Competitive, Wingman)
|
- ⚠️ **BLOCKED by backend**: Rank tier filter (CS2 Premier rating ranges) - UI present with "Coming Soon" badge
|
||||||
- Player name search (autocomplete)
|
- ⚠️ **BLOCKED by backend**: Game mode filter (Premier, Competitive, Wingman) - UI present with "Coming Soon" badge
|
||||||
- Result filter (win/loss/tie)
|
- ⚠️ **BLOCKED by backend**: Player name search autocomplete - No `/players/search` endpoint exists, UI shows "Coming Soon" badge
|
||||||
- [ ] Matches table/grid view toggle:
|
- **Current functionality**: Basic match ID/share code search works
|
||||||
- Table: columns for date, map, score, duration, players, actions
|
- **Backend needs**: New endpoint `GET /players/search?q={query}&limit={limit}` returning player suggestions
|
||||||
- Grid: card-based layout with match thumbnails
|
- [x] Matches table/grid view toggle:
|
||||||
- Virtualized scrolling for performance (svelte-virtual-list or custom)
|
- [x] Table: columns for date, map, score, duration, players, actions
|
||||||
- [ ] Sorting controls (date, duration, score difference)
|
- [x] Grid: card-based layout with match thumbnails
|
||||||
- [ ] Pagination or infinite scroll
|
- [x] View preference persisted to localStorage
|
||||||
- [ ] Empty state with helpful message and search tips
|
- [x] Infinite scroll implemented (virtualized scrolling not needed - infinite scroll is the better UX)
|
||||||
- [ ] Export filtered results (CSV/JSON download)
|
- [x] Sorting controls (date, duration, score difference)
|
||||||
|
- [x] Infinite scroll with intersection observer
|
||||||
|
- [x] Empty state with helpful message and search tips
|
||||||
|
- [x] Export filtered results (CSV/JSON download)
|
||||||
|
- [x] Share code input component for demo parsing
|
||||||
|
- [x] Active filters display with clear functionality
|
||||||
|
|
||||||
|
**Backend API Blockers** (3 features waiting for backend support):
|
||||||
|
|
||||||
|
1. **Player name search** - Requires `GET /players/search?q={query}&limit={limit}`
|
||||||
|
2. **Rank tier filter** - Requires rank filtering in `GET /matches` endpoint
|
||||||
|
3. **Game mode filter** - Requires game_mode field and filtering in `GET /matches` endpoint
|
||||||
|
|
||||||
### 5.3 Player Profile (`/player/[id]` - `src/routes/player/[id]/+page.svelte`) ✅ COMPLETE
|
### 5.3 Player Profile (`/player/[id]` - `src/routes/player/[id]/+page.svelte`) ✅ COMPLETE
|
||||||
|
|
||||||
@@ -253,31 +268,38 @@
|
|||||||
- [ ] Share profile button:
|
- [ ] Share profile button:
|
||||||
- ⚠️ **Deferred to future update** (generate shareable link/image)
|
- ⚠️ **Deferred to future update** (generate shareable link/image)
|
||||||
|
|
||||||
### 5.4 Match Overview (`/match/[id]` - `src/routes/match/[id]/+page.svelte`)
|
### 5.4 Match Overview (`/match/[id]` - `src/routes/match/[id]/+page.svelte`) ✅ SUBSTANTIALLY COMPLETE
|
||||||
|
|
||||||
- [ ] Match header:
|
- [x] Match header (implemented in `+layout.svelte`):
|
||||||
- Map name with background image
|
- ✅ Map name with background image (full-width header)
|
||||||
- Final score (large, prominent)
|
- ✅ Multi-layer gradient overlays for depth
|
||||||
- Match date, duration, game mode
|
- ✅ Final score (large, prominent with team colors)
|
||||||
- Download demo button (if available)
|
- ✅ Match date, duration, game mode, tick rate
|
||||||
- [ ] Tab navigation (sticky on scroll):
|
- ✅ Download demo button (functional with Steam protocol link)
|
||||||
- Overview (default), Economy, Details, Flashes, Damage, Chat
|
- ✅ Back to matches navigation
|
||||||
- Active tab indicator with smooth transition
|
- ✅ Demo parsed status badge
|
||||||
- [ ] Scoreboard component (`MatchScoreboard.svelte`):
|
- [x] Tab navigation:
|
||||||
- Two teams (T/CT) with color coding
|
- ✅ Overview, Economy, Details, Weapons, Flashes, Damage, Chat
|
||||||
- Player rows: name, K/D/A, ADR, HS%, rating, MVPs
|
- ✅ Sticky tabs with backdrop blur
|
||||||
- Sortable columns
|
- ✅ Active tab indicator
|
||||||
- Highlight top performers (gold/silver/bronze indicators)
|
- [x] Team statistics overview cards:
|
||||||
- Click player name to navigate to player profile
|
- ✅ Both teams with T/CT color coding
|
||||||
- [ ] Round history timeline:
|
- ✅ Team K/D, ADR, KAST, total kills, average Premier rating
|
||||||
- Horizontal timeline showing all rounds (MR12: 24 max rounds + OT)
|
- [x] Scoreboard component:
|
||||||
- Icons indicating round winner (T/CT bomb icons)
|
- ✅ Two teams with color coding
|
||||||
- Round win reason (elimination, defuse, time, bomb explosion)
|
- ✅ Player rows: name, K/D/A, ADR, HS%, KAST%, rating, MVPs
|
||||||
- Click round to show detailed events (kill feed, economy)
|
- ✅ Sortable by kills (default)
|
||||||
- [ ] Win probability chart (optional advanced feature):
|
- ✅ Top performer indicators (Trophy icons)
|
||||||
- Line graph showing round-by-round win probability
|
- ✅ Clickable player names → player profiles
|
||||||
- Based on economy, players alive, time remaining
|
- ✅ Player color indicators (from game)
|
||||||
- [ ] Map callouts reference (expandable panel)
|
- [x] Round history timeline:
|
||||||
|
- ✅ RoundTimeline component with MR12 support (24 rounds + OT)
|
||||||
|
- ⚠️ Round win reason indicators - **Deferred** (data not in current API response)
|
||||||
|
- ⚠️ Click round for detailed events - **Deferred**
|
||||||
|
- [ ] Win probability chart:
|
||||||
|
- ❌ **Deferred to future update** (advanced feature, requires economy simulation)
|
||||||
|
- [ ] Map callouts reference:
|
||||||
|
- ❌ **Deferred to future update** (requires callout data/images)
|
||||||
|
|
||||||
### 5.5 Economy Tab (`/match/[id]/economy` - `src/routes/match/[id]/economy/+page.svelte`) ✅ COMPLETE
|
### 5.5 Economy Tab (`/match/[id]/economy` - `src/routes/match/[id]/economy/+page.svelte`) ✅ COMPLETE
|
||||||
|
|
||||||
@@ -314,46 +336,51 @@
|
|||||||
- [ ] Advanced metrics:
|
- [ ] Advanced metrics:
|
||||||
- ⚠️ **Deferred to future update**
|
- ⚠️ **Deferred to future update**
|
||||||
|
|
||||||
### 5.7 Flashes Tab (`/match/[id]/flashes` - `src/routes/match/[id]/flashes/+page.svelte`)
|
### 5.7 Flashes Tab (`/match/[id]/flashes` - `src/routes/match/[id]/flashes/+page.svelte`) ⚠️ BASIC IMPLEMENTATION COMPLETE
|
||||||
|
|
||||||
- [ ] Flash effectiveness leaderboard:
|
- [x] Flash effectiveness leaderboard:
|
||||||
- Players ranked by total enemies blinded
|
- ✅ Players ranked by total enemies blinded
|
||||||
- Average blind duration per flash
|
- ✅ Average blind duration per flash
|
||||||
- Flash assists (blinded enemy killed by teammate)
|
- ✅ Flash assists (blinded enemy killed by teammate)
|
||||||
- Self-flash count (mistakes)
|
- ✅ Self-flash count displayed
|
||||||
|
- ✅ Team-specific flash stats tables (T vs CT)
|
||||||
|
- ✅ Summary stats cards (Total Blinded, Flash Assists, Blind Time)
|
||||||
- [ ] Flash timeline visualization:
|
- [ ] Flash timeline visualization:
|
||||||
- Round-by-round flash events
|
- ❌ Round-by-round flash events - **Deferred to future update**
|
||||||
- Pop flash detection (short reaction time)
|
- ❌ Pop flash detection (short reaction time) - **Deferred**
|
||||||
- Team flashes vs solo flashes
|
- ❌ Team flashes vs solo flashes - **Deferred**
|
||||||
- [ ] Heatmap overlay (advanced):
|
- [ ] Heatmap overlay (advanced):
|
||||||
- Map with flash throw positions
|
- ❌ Map with flash throw positions - **Deferred to future update**
|
||||||
- Color intensity = effectiveness
|
- ❌ Color intensity = effectiveness - **Deferred**
|
||||||
- Clickable markers showing flash details
|
- ❌ Clickable markers showing flash details - **Deferred**
|
||||||
- [ ] CS2-specific features:
|
- [ ] CS2-specific features:
|
||||||
- Volumetric smoke interactions (flashes through smoke)
|
- ❌ Volumetric smoke interactions (flashes through smoke) - **Deferred**
|
||||||
- New flash mechanics tracking
|
- ❌ New flash mechanics tracking - **Deferred**
|
||||||
|
|
||||||
### 5.8 Damage Tab (`/match/[id]/damage` - `src/routes/match/[id]/damage/+page.svelte`)
|
### 5.8 Damage Tab (`/match/[id]/damage` - `src/routes/match/[id]/damage/+page.svelte`) ⚠️ BASIC IMPLEMENTATION COMPLETE
|
||||||
|
|
||||||
- [ ] Damage dealt/received summary:
|
- [x] Damage dealt/received summary:
|
||||||
- Per-player total damage
|
- ✅ Per-player total damage in sortable table
|
||||||
- Damage per round (line chart)
|
- ✅ Team damage stat cards (T vs CT)
|
||||||
- Weapon damage breakdown
|
- ✅ Top 3 damage dealers cards
|
||||||
|
- ❌ Damage per round (line chart) - **Deferred to future update**
|
||||||
|
- ❌ Weapon damage breakdown - **Deferred**
|
||||||
|
- [x] Utility damage:
|
||||||
|
- ✅ Utility damage distribution pie chart (HE + Fire)
|
||||||
|
- ❌ HE grenade damage tracking details - **Deferred**
|
||||||
|
- ❌ Molotov/Incendiary damage over time - **Deferred**
|
||||||
|
- ❌ CS2: Volumetric smoke damage interactions - **Deferred**
|
||||||
- [ ] Hit group analysis:
|
- [ ] Hit group analysis:
|
||||||
- Pie chart: headshots, chest, legs, arms
|
- ❌ Pie chart: headshots, chest, legs, arms - **Deferred to future update**
|
||||||
- Headshot percentage ranking
|
- ❌ Headshot percentage ranking - **Deferred**
|
||||||
- [ ] Damage heatmap (Canvas/WebGL):
|
- [ ] Damage heatmap (Canvas/WebGL):
|
||||||
- Map overlay showing where damage occurred
|
- ❌ Map overlay showing where damage occurred - **Deferred to future update**
|
||||||
- Click to filter by player, weapon, round
|
- ❌ Click to filter by player, weapon, round - **Deferred**
|
||||||
- Color intensity = damage amount
|
- ❌ Color intensity = damage amount - **Deferred**
|
||||||
- Toggle between dealt/received damage
|
- ❌ Toggle between dealt/received damage - **Deferred**
|
||||||
- [ ] Engagement distance chart:
|
- [ ] Engagement distance chart:
|
||||||
- Histogram showing damage at various ranges
|
- ❌ Histogram showing damage at various ranges - **Deferred to future update**
|
||||||
- Optimal range analysis per weapon
|
- ❌ Optimal range analysis per weapon - **Deferred**
|
||||||
- [ ] Utility damage:
|
|
||||||
- HE grenade damage tracking
|
|
||||||
- Molotov/Incendiary damage over time
|
|
||||||
- CS2: Volumetric smoke damage interactions
|
|
||||||
|
|
||||||
### 5.9 Chat Tab (`/match/[id]/chat` - `src/routes/match/[id]/chat/+page.svelte`) ✅ COMPLETE
|
### 5.9 Chat Tab (`/match/[id]/chat` - `src/routes/match/[id]/chat/+page.svelte`) ✅ COMPLETE
|
||||||
|
|
||||||
@@ -377,24 +404,31 @@
|
|||||||
- ⚠️ Translation toggle: **Deferred to future update**
|
- ⚠️ Translation toggle: **Deferred to future update**
|
||||||
- ⚠️ Toxic language detection: **Deferred to future update**
|
- ⚠️ Toxic language detection: **Deferred to future update**
|
||||||
|
|
||||||
### 5.10 CS2-Exclusive Features
|
### 5.10 CS2-Exclusive Features ✅ SUBSTANTIALLY COMPLETE
|
||||||
|
|
||||||
- [ ] MR12 format awareness:
|
- [x] MR12 format awareness:
|
||||||
- Update all round displays to reflect 24-round max (12-12)
|
- ✅ RoundTimeline component dynamically calculates halftime (round 12 for MR12, round 15 for MR15)
|
||||||
- Adjust overtime logic and display
|
- ✅ Economy tab dynamically calculates halftime based on match.max_rounds
|
||||||
- [ ] New utility tracking:
|
- ✅ All round displays use max_rounds from match data (24 for MR12, 30 for MR15)
|
||||||
- Volumetric smokes (coverage area, bloom effect)
|
- ✅ ADR calculations use dynamic max_rounds
|
||||||
- Updated grenade mechanics
|
- ⚠️ Overtime display logic not yet implemented (deferred - requires overtime data from backend)
|
||||||
- Molotov spread and duration
|
- [x] Premier mode specifics:
|
||||||
- [ ] Premier mode specifics:
|
- ✅ CS2 rating system fully implemented (0-30,000 range)
|
||||||
- CS2 rating system (different from CS:GO ranks)
|
- ✅ PremierRatingBadge component with tier colors (Beginner/Intermediate/Advanced/Expert/Elite/Legendary)
|
||||||
- Skill group progression tracking
|
- ✅ Rating change tracking (show +/- rating difference)
|
||||||
- [ ] Updated economy values:
|
- ✅ Automatic detection of Skill Group (0-18) vs CS Rating (>1000) based on match date and game mode
|
||||||
- CS2 weapon prices and economy changes
|
- ✅ RankIcon component for displaying CS:GO legacy skill groups
|
||||||
- Loss bonus adjustments
|
- ✅ Formatters handle full 0-30,000 rating range with color-coded tiers
|
||||||
- [ ] New weapon statistics:
|
- ⚠️ **DEFERRED**: New utility tracking (requires backend support):
|
||||||
- Track kills with CS2-exclusive weapons
|
- Volumetric smokes (coverage area, bloom effect) - **Backend needs smoke effectiveness data**
|
||||||
- Update weapon icons and names
|
- Updated grenade mechanics tracking - **Requires additional API fields**
|
||||||
|
- Molotov spread and duration analysis - **Requires positional data**
|
||||||
|
- ⚠️ **OUT OF SCOPE**: Updated economy values:
|
||||||
|
- CS2 weapon prices and economy changes - **Backend handles this automatically**
|
||||||
|
- Loss bonus adjustments - **Backend parses demo with correct CS2 values**
|
||||||
|
- ⚠️ **DEFERRED**: New weapon statistics:
|
||||||
|
- Track kills with CS2-exclusive weapons - **Weapons endpoint exists, visualization deferred**
|
||||||
|
- Update weapon icons and names - **Would require asset updates**
|
||||||
|
|
||||||
### 5.10 Shared Components Library (`src/lib/components/`) - IN PROGRESS
|
### 5.10 Shared Components Library (`src/lib/components/`) - IN PROGRESS
|
||||||
|
|
||||||
@@ -451,6 +485,93 @@
|
|||||||
- `Tabs.svelte`: tab navigation component
|
- `Tabs.svelte`: tab navigation component
|
||||||
- `Accordion.svelte`: expandable sections
|
- `Accordion.svelte`: expandable sections
|
||||||
|
|
||||||
|
## Phase 5.12 – Critical TypeScript Errors (BLOCKING) ✅ COMPLETE
|
||||||
|
|
||||||
|
**Status**: ✅ All fixed - ready for production
|
||||||
|
**Priority**: P0 (Highest)
|
||||||
|
**Completion Date**: 2025-11-13
|
||||||
|
|
||||||
|
### TrackPlayerModal API Signature Mismatches ✅
|
||||||
|
|
||||||
|
**File**: `src/lib/components/player/TrackPlayerModal.svelte`
|
||||||
|
|
||||||
|
- [x] Fixed `trackPlayer()` call - Removed `shareCode` parameter to match API signature
|
||||||
|
- [x] Fixed `untrackPlayer()` call - Removed `authCode` parameter (API doesn't require it)
|
||||||
|
- [x] Fixed Modal binding - Changed `bind:isOpen` to `bind:open`
|
||||||
|
- [x] **Solution Applied**: Removed shareCode input from UI, API accepts (steamId, authCode) only
|
||||||
|
- [x] Added event callback props (`ontracked`, `onuntracked`) to support Svelte 5 pattern
|
||||||
|
|
||||||
|
**Result**: Player tracking feature now works correctly in strict TypeScript mode
|
||||||
|
|
||||||
|
### Player Profile Type Inconsistencies ✅
|
||||||
|
|
||||||
|
**File**: `src/routes/player/[id]/+page.svelte`
|
||||||
|
|
||||||
|
- [x] Fixed `addRecentPlayer()` type mismatch - Changed `PlayerMeta.id` from `number` to `string`
|
||||||
|
- [x] Fixed `profile.vac_count` reference - Added to `PlayerMeta` interface
|
||||||
|
- [x] Fixed `profile.vac_date` reference - Added to `PlayerMeta` interface
|
||||||
|
- [x] Fixed `profile.game_ban_count` reference - Added to `PlayerMeta` interface
|
||||||
|
- [x] Fixed `profile.game_ban_date` reference - Added to `PlayerMeta` interface
|
||||||
|
- [x] **Solution Applied**: Extended `PlayerMeta` interface with ban fields and dates
|
||||||
|
- [x] Updated `preferences.ts` store - Changed `favoritePlayers` from `number[]` to `string[]`
|
||||||
|
- [x] Updated mock fixtures - Changed `mockPlayerMeta.id` to string type
|
||||||
|
|
||||||
|
**Result**: VAC/ban badges display correctly, recent players feature works, type consistency maintained
|
||||||
|
|
||||||
|
### DataTable Generic Type Constraints ✅
|
||||||
|
|
||||||
|
**Files**:
|
||||||
|
|
||||||
|
- `src/routes/match/[id]/details/+page.svelte`
|
||||||
|
- `src/routes/match/[id]/weapons/+page.svelte`
|
||||||
|
|
||||||
|
- [x] Fixed DataTable column definitions - Added explicit type annotations to render/format functions
|
||||||
|
- [x] Created type aliases (`PlayerWithStats`, `PlayerWeapon`) for cleaner column definitions
|
||||||
|
- [x] Used `unknown` type for parameters to satisfy generic constraints
|
||||||
|
- [x] **Solution Applied**: Explicit typing on arrow function parameters
|
||||||
|
|
||||||
|
**Result**: Type safety fully restored, no implicit any types
|
||||||
|
|
||||||
|
### Chart.js Fill Property Type Errors ✅
|
||||||
|
|
||||||
|
**File**: `src/routes/match/[id]/economy/+page.svelte:156, 166`
|
||||||
|
|
||||||
|
- [x] Fixed `fill: 'origin'` type error - Used `@ts-expect-error` with explanatory comment
|
||||||
|
- [x] **Solution Applied**: Suppressed incorrect Chart.js type definition (fill accepts 'origin' value)
|
||||||
|
- [x] Documented that Chart.js types are outdated but code is correct
|
||||||
|
|
||||||
|
**Result**: Build succeeds, chart fill behavior works as expected
|
||||||
|
|
||||||
|
### Missing Environment Module ✅
|
||||||
|
|
||||||
|
**File**: `src/routes/api/[...path]/+server.ts`
|
||||||
|
|
||||||
|
- [x] Fixed import error - Changed from `$env/dynamic/private` to `import.meta.env`
|
||||||
|
- [x] **Solution Applied**: Use Vite's `import.meta.env.VITE_API_BASE_URL` for environment variables
|
||||||
|
- [x] Updated comment to reflect correct approach for VITE\_ prefixed vars
|
||||||
|
|
||||||
|
**Result**: API proxy route now works correctly in all environments
|
||||||
|
|
||||||
|
### Button Component Enhancements ✅
|
||||||
|
|
||||||
|
**File**: `src/lib/components/ui/Button.svelte`
|
||||||
|
|
||||||
|
- [x] Added `target` and `rel` props for external links
|
||||||
|
- [x] Updated template to pass through anchor attributes
|
||||||
|
- [x] Fixed player profile button variant (changed 'success' to 'secondary')
|
||||||
|
|
||||||
|
**Result**: External links (Steam profile) now work correctly with security attributes
|
||||||
|
|
||||||
|
### Other TypeScript Issues ✅
|
||||||
|
|
||||||
|
- [x] Fixed unused variable warnings - Used `_value` prefix for intentionally unused parameters
|
||||||
|
- [x] Resolved all implicit any types - Added explicit type annotations
|
||||||
|
- [x] Ran `npm run check` - Zero errors, zero warnings
|
||||||
|
|
||||||
|
**Success Criteria Met**: ✅ `npm run check` exits with zero errors and zero warnings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Phase 6 – Localization & Personalization
|
## Phase 6 – Localization & Personalization
|
||||||
|
|
||||||
- [ ] Integrate `sveltekit-i18n` (or equivalent) for multi-language support, starting with English plus priority locales.
|
- [ ] Integrate `sveltekit-i18n` (or equivalent) for multi-language support, starting with English plus priority locales.
|
||||||
@@ -469,11 +590,70 @@
|
|||||||
|
|
||||||
## Phase 8 – Testing & Quality Assurance
|
## Phase 8 – Testing & Quality Assurance
|
||||||
|
|
||||||
- [ ] Write unit tests for components, stores, and utilities (Vitest + Testing Library).
|
**Current Status**: ⚠️ Minimal coverage (~5% of planned tests)
|
||||||
- [ ] Develop integration/E2E scenarios (Playwright) covering match search, player view, and match detail tabs.
|
**Target**: 80%+ coverage for unit/integration tests
|
||||||
|
|
||||||
|
### Unit Tests (0% Complete)
|
||||||
|
|
||||||
|
- [ ] Write component tests (Vitest + Testing Library):
|
||||||
|
- [ ] UI components: Button, Badge, Card, Modal, Tabs, Tooltip, Skeleton, Toast
|
||||||
|
- [ ] Chart components: LineChart, BarChart, PieChart
|
||||||
|
- [ ] Data display: DataTable, RoundTimeline
|
||||||
|
- [ ] Match components: MatchCard, ShareCodeInput
|
||||||
|
- [ ] Player components: PlayerCard, TrackPlayerModal, RecentPlayers
|
||||||
|
- [ ] Layout components: Header, Footer, SearchBar, ThemeToggle
|
||||||
|
- [ ] Write store tests:
|
||||||
|
- [ ] preferences.ts (theme, favorites, settings persistence)
|
||||||
|
- [ ] search.ts (query, filters, recent searches)
|
||||||
|
- [ ] toast.ts (notification queue, auto-dismiss)
|
||||||
|
- [ ] Write utility function tests:
|
||||||
|
- [ ] formatters.ts (date, number, rank formatting)
|
||||||
|
- [ ] validators.ts (Steam ID, share code validation)
|
||||||
|
- [ ] constants.ts (buy type thresholds, map names)
|
||||||
|
|
||||||
|
**Current Coverage**: 0 test files in `/tests/unit/`
|
||||||
|
|
||||||
|
### Integration Tests (0% Complete)
|
||||||
|
|
||||||
|
- [ ] Develop integration scenarios:
|
||||||
|
- [ ] Match search flow (filters → results → detail)
|
||||||
|
- [ ] Player profile navigation (search → profile → match)
|
||||||
|
- [ ] Match detail tab switching (overview → economy → details → chat)
|
||||||
|
- [ ] Infinite scroll behavior
|
||||||
|
- [ ] Export functionality (CSV/JSON)
|
||||||
|
- [ ] Share code submission
|
||||||
|
- [ ] Recent players tracking
|
||||||
|
- [ ] Theme persistence
|
||||||
|
|
||||||
|
**Current Coverage**: 0 test files in `/tests/integration/`
|
||||||
|
|
||||||
|
### E2E Tests (~1% Complete)
|
||||||
|
|
||||||
|
- [x] Basic homepage test (`tests/e2e/home.test.ts`)
|
||||||
|
- [ ] Develop comprehensive E2E scenarios (Playwright):
|
||||||
|
- [ ] Match search and filtering
|
||||||
|
- [ ] Player profile viewing
|
||||||
|
- [ ] Match detail tab navigation
|
||||||
|
- [ ] Share code parsing flow
|
||||||
|
- [ ] Export match data
|
||||||
|
- [ ] Recent players functionality
|
||||||
|
- [ ] Mobile responsive behavior
|
||||||
- [ ] Set up visual regression snapshots for critical pages via Playwright or Loki.
|
- [ ] Set up visual regression snapshots for critical pages via Playwright or Loki.
|
||||||
|
- [ ] Cross-browser testing (Chrome, Firefox, Safari)
|
||||||
|
|
||||||
|
**Current Coverage**: 1 test file with 2 basic tests
|
||||||
|
|
||||||
|
### Performance & Load Testing
|
||||||
|
|
||||||
- [ ] Build load-testing scripts for APIs powering live dashboards (k6 or artillery).
|
- [ ] Build load-testing scripts for APIs powering live dashboards (k6 or artillery).
|
||||||
|
- [ ] Set up Lighthouse CI for automated performance checks
|
||||||
|
- [ ] Bundle size monitoring and regression detection
|
||||||
|
|
||||||
|
### QA Process
|
||||||
|
|
||||||
- [ ] Coordinate QA playbook: test matrices, release checklists, and bug triage workflow.
|
- [ ] Coordinate QA playbook: test matrices, release checklists, and bug triage workflow.
|
||||||
|
- [ ] Establish code review checklist
|
||||||
|
- [ ] Create testing documentation
|
||||||
|
|
||||||
## Phase 9 – Deployment & Release Strategy
|
## Phase 9 – Deployment & Release Strategy
|
||||||
|
|
||||||
@@ -878,15 +1058,97 @@ VITE_PLAUSIBLE_DOMAIN=cs2.wtf
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated**: 2025-11-04
|
**Last Updated**: 2025-11-13
|
||||||
**Current Phase**: Phase 5 (Feature Delivery) - IN PROGRESS (50% Complete)
|
**Current Phase**: Phase 5 (Feature Delivery) - IN PROGRESS (75% Complete)
|
||||||
**Completed Phases**: Phase 0 (Planning), Phase 1 (Technical Foundations), Phase 2 (Design System), Phase 3 (Domain Modeling), Phase 4 (Application Architecture)
|
**Completed Phases**: Phase 0 (Planning), Phase 1 (Technical Foundations), Phase 2 (Design System), Phase 3 (Domain Modeling), Phase 4 (Application Architecture), Phase 5.12 (Critical TypeScript Fixes)
|
||||||
**Next Milestone**: Complete remaining match detail tabs (Flashes, Damage), enhance player profile with charts
|
**Next Milestone**: Phase 6 (Localization) or Phase 8 (Testing & QA)
|
||||||
**Recent Progress**:
|
**Blocking Issues**: None - All critical issues resolved ✅
|
||||||
|
|
||||||
|
**Recent Progress** (2025-11-13 Session 2):
|
||||||
|
|
||||||
|
- ✅ **Section 5.2 (Matches Listing) completion audit**:
|
||||||
|
- Investigated backend API for player search capability
|
||||||
|
- Confirmed 3 features blocked by backend API support:
|
||||||
|
1. Player name search/autocomplete (no `/players/search` endpoint)
|
||||||
|
2. Rank tier filter (no rank filtering in matches endpoint)
|
||||||
|
3. Game mode filter (no game_mode field in API)
|
||||||
|
- Added "Coming Soon" badge to search input with tooltip explanation
|
||||||
|
- Updated placeholder text to clarify current search capability (match ID/share code only)
|
||||||
|
- Documented all backend blockers in Section 5.2 with required API changes
|
||||||
|
- Marked Section 5.2 as COMPLETE with documented blockers
|
||||||
|
- Cleaned up outdated TypeScript error warnings from Sections 5.3, 5.5, 5.6
|
||||||
|
|
||||||
|
- ✅ **Section 5.10 (CS2-Exclusive Features) implementation**:
|
||||||
|
- Fixed MR12/MR15 halftime calculation to be dynamic based on max_rounds:
|
||||||
|
- Updated RoundTimeline component to accept maxRounds prop
|
||||||
|
- Updated Economy tab to calculate halfPoint from match.max_rounds
|
||||||
|
- MR12 (24 rounds): halftime after round 12
|
||||||
|
- MR15 (30 rounds): halftime after round 15
|
||||||
|
- Verified Premier rating system fully supports 0-30,000 range:
|
||||||
|
- PremierRatingBadge component with 6 tier colors
|
||||||
|
- Rating change tracking with +/- display
|
||||||
|
- Automatic detection of Skill Group vs CS Rating
|
||||||
|
- Documented deferred features (volumetric smokes, weapon stats) requiring backend support
|
||||||
|
- Marked Section 5.10 as SUBSTANTIALLY COMPLETE
|
||||||
|
|
||||||
|
**Recent Progress** (2025-11-13 Session 1):
|
||||||
|
|
||||||
|
- ✅ **FIXED: All 12 critical TypeScript errors resolved** - Project now compiles with zero errors ✅
|
||||||
|
- Fixed TrackPlayerModal API signature mismatches
|
||||||
|
- Fixed Player Profile type inconsistencies (PlayerMeta extended with ban fields)
|
||||||
|
- Fixed DataTable generic type constraints with explicit typing
|
||||||
|
- Fixed Chart.js fill property errors with @ts-expect-error
|
||||||
|
- Fixed environment module imports (switched to import.meta.env)
|
||||||
|
- Added target/rel props to Button component
|
||||||
|
- ✅ **Homepage enhancements completed**:
|
||||||
|
- Added "Most Played Maps" section with pie chart (top 7 maps from last 50 matches)
|
||||||
|
- Added "Community Statistics" dashboard with recent activity cards
|
||||||
|
- Added processing matches indicator with pulsing animation
|
||||||
|
- Enhanced page loader to calculate map statistics dynamically
|
||||||
|
- ✅ **Match Overview enhancements**:
|
||||||
|
- Download demo button now functional (uses Steam protocol links)
|
||||||
|
- Map background header already implemented with beautiful gradients
|
||||||
|
- ⚠️ Test coverage still severely lacking (<5% actual vs 80% target)
|
||||||
|
|
||||||
|
**Earlier Progress** (Since 2025-11-04):
|
||||||
|
|
||||||
|
- ✅ Comprehensive project analysis completed
|
||||||
|
- ✅ Matches listing substantially complete (95%) - all core features done
|
||||||
|
- ✅ Flashes tab basic implementation complete with leaderboard and team stats
|
||||||
|
- ✅ Damage tab basic implementation complete with summary tables and pie chart
|
||||||
|
|
||||||
|
**Previously Completed**:
|
||||||
|
|
||||||
- ✅ Implemented chart components (Line, Bar, Pie) with Chart.js
|
- ✅ Implemented chart components (Line, Bar, Pie) with Chart.js
|
||||||
- ✅ Created sortable DataTable component
|
- ✅ Created sortable DataTable component
|
||||||
- ✅ Match Economy tab with buy type analysis and equipment value charts
|
- ✅ Match Economy tab with buy type analysis and equipment value charts
|
||||||
- ✅ Match Details tab with multi-kill distribution and top performers
|
- ✅ Match Details tab with multi-kill distribution and top performers
|
||||||
- ✅ Match Chat tab with filtering, search, and round grouping
|
- ✅ Match Chat tab with filtering, search, and round grouping
|
||||||
- ⚠️ Flashes and Damage tabs deferred for future implementation
|
- ✅ Player profile with performance charts and utility stats
|
||||||
|
- ✅ Homepage with featured matches carousel and stats dashboard
|
||||||
|
|
||||||
|
**Known Issues**:
|
||||||
|
|
||||||
|
1. **TypeScript Errors**: ✅ **ALL FIXED** - Zero errors, zero warnings (as of 2025-11-13)
|
||||||
|
|
||||||
|
2. **Technical Debt**:
|
||||||
|
- Zero unit test coverage (0 test files in `/tests/unit/`)
|
||||||
|
- Zero integration test coverage (0 test files in `/tests/integration/`)
|
||||||
|
- Minimal E2E coverage (1 file with 2 basic tests)
|
||||||
|
- TODO comments in transformers (round winner/weapon kills not extracted)
|
||||||
|
- Magic numbers for buy type thresholds (should be constants)
|
||||||
|
|
||||||
|
3. **Deferred Advanced Features**:
|
||||||
|
- Damage/flash heatmaps (Canvas/WebGL visualization)
|
||||||
|
- Hit group analysis charts
|
||||||
|
- Engagement distance histograms
|
||||||
|
- Flash/damage timeline visualizations
|
||||||
|
- Live match indicators (backend support needed)
|
||||||
|
- Volumetric smoke tracking (CS2-specific)
|
||||||
|
|
||||||
|
**Priority Actions**:
|
||||||
|
|
||||||
|
1. **Immediate** (1-2 days): Fix all TypeScript errors (Phase 5.12)
|
||||||
|
2. **Short-term** (1 week): Implement test suite (unit + integration + E2E)
|
||||||
|
3. **Medium-term** (2 weeks): Complete advanced visualizations (heatmaps, timelines)
|
||||||
|
4. **Long-term** (1 month): Phase 6 (Localization), Phase 7 (Performance)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const playersAPI = {
|
|||||||
|
|
||||||
// Transform to PlayerMeta format
|
// Transform to PlayerMeta format
|
||||||
const playerMeta: PlayerMeta = {
|
const playerMeta: PlayerMeta = {
|
||||||
id: parseInt(player.id, 10),
|
id: player.id, // Keep as string for uint64 precision
|
||||||
name: player.name,
|
name: player.name,
|
||||||
avatar: player.avatar, // Already transformed by transformPlayerProfile
|
avatar: player.avatar, // Already transformed by transformPlayerProfile
|
||||||
recent_matches: recentMatches.length,
|
recent_matches: recentMatches.length,
|
||||||
@@ -74,7 +74,12 @@ export const playersAPI = {
|
|||||||
avg_kills: avgKills,
|
avg_kills: avgKills,
|
||||||
avg_deaths: avgDeaths,
|
avg_deaths: avgDeaths,
|
||||||
avg_kast: recentMatches.length > 0 ? totalKast / recentMatches.length : 0, // Placeholder KAST calculation
|
avg_kast: recentMatches.length > 0 ? totalKast / recentMatches.length : 0, // Placeholder KAST calculation
|
||||||
win_rate: winRate
|
win_rate: winRate,
|
||||||
|
vac_count: player.vac_count,
|
||||||
|
vac_date: player.vac_date,
|
||||||
|
game_ban_count: player.game_ban_count,
|
||||||
|
game_ban_date: player.game_ban_date,
|
||||||
|
tracked: player.tracked
|
||||||
};
|
};
|
||||||
|
|
||||||
return playerMeta;
|
return playerMeta;
|
||||||
|
|||||||
@@ -4,7 +4,12 @@
|
|||||||
import Card from '$lib/components/ui/Card.svelte';
|
import Card from '$lib/components/ui/Card.svelte';
|
||||||
import type { RoundDetail } from '$lib/types/RoundStats';
|
import type { RoundDetail } from '$lib/types/RoundStats';
|
||||||
|
|
||||||
let { rounds }: { rounds: RoundDetail[] } = $props();
|
let { rounds, maxRounds = 24 }: { rounds: RoundDetail[]; maxRounds?: number } = $props();
|
||||||
|
|
||||||
|
// Calculate halftime round based on max_rounds
|
||||||
|
// MR12 (24 rounds): halftime after round 12
|
||||||
|
// MR15 (30 rounds): halftime after round 15
|
||||||
|
const halftimeRound = $derived(maxRounds === 30 ? 15 : 12);
|
||||||
|
|
||||||
// State for hover/click details
|
// State for hover/click details
|
||||||
let selectedRound = $state<number | null>(null);
|
let selectedRound = $state<number | null>(null);
|
||||||
@@ -174,10 +179,13 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Half marker (round 13 for MR12) -->
|
<!-- Half marker (dynamic based on MR12/MR15) -->
|
||||||
{#if rounds.length > 12}
|
{#if rounds.length > halftimeRound}
|
||||||
<div class="relative mt-2 flex gap-1">
|
<div class="relative mt-2 flex gap-1">
|
||||||
<div class="ml-[calc(60px*12-30px)] w-[60px] text-center">
|
<div
|
||||||
|
class="w-[60px] text-center"
|
||||||
|
style="margin-left: calc(60px * {halftimeRound} - 30px);"
|
||||||
|
>
|
||||||
<Badge variant="info" size="sm">Halftime</Badge>
|
<Badge variant="info" size="sm">Halftime</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,15 +8,23 @@
|
|||||||
playerId: string;
|
playerId: string;
|
||||||
playerName: string;
|
playerName: string;
|
||||||
isTracked: boolean;
|
isTracked: boolean;
|
||||||
isOpen: boolean;
|
open: boolean;
|
||||||
|
ontracked?: () => void;
|
||||||
|
onuntracked?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { playerId, playerName, isTracked, isOpen = $bindable() }: Props = $props();
|
let {
|
||||||
|
playerId,
|
||||||
|
playerName,
|
||||||
|
isTracked,
|
||||||
|
open = $bindable(),
|
||||||
|
ontracked,
|
||||||
|
onuntracked
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let authCode = $state('');
|
let authCode = $state('');
|
||||||
let shareCode = $state('');
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let error = $state('');
|
let error = $state('');
|
||||||
|
|
||||||
@@ -30,10 +38,11 @@
|
|||||||
error = '';
|
error = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await playersAPI.trackPlayer(playerId, authCode, shareCode || undefined);
|
await playersAPI.trackPlayer(playerId, authCode);
|
||||||
toast.success('Player tracking activated successfully!');
|
toast.success('Player tracking activated successfully!');
|
||||||
isOpen = false;
|
open = false;
|
||||||
dispatch('tracked');
|
dispatch('tracked');
|
||||||
|
ontracked?.();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
error = err instanceof Error ? err.message : 'Failed to track player';
|
error = err instanceof Error ? err.message : 'Failed to track player';
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
@@ -43,19 +52,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleUntrack() {
|
async function handleUntrack() {
|
||||||
if (!authCode.trim()) {
|
|
||||||
error = 'Auth code is required to untrack';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
error = '';
|
error = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await playersAPI.untrackPlayer(playerId, authCode);
|
await playersAPI.untrackPlayer(playerId);
|
||||||
toast.success('Player tracking removed successfully');
|
toast.success('Player tracking removed successfully');
|
||||||
isOpen = false;
|
open = false;
|
||||||
dispatch('untracked');
|
dispatch('untracked');
|
||||||
|
onuntracked?.();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
error = err instanceof Error ? err.message : 'Failed to untrack player';
|
error = err instanceof Error ? err.message : 'Failed to untrack player';
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
@@ -65,14 +70,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
isOpen = false;
|
open = false;
|
||||||
authCode = '';
|
authCode = '';
|
||||||
shareCode = '';
|
|
||||||
error = '';
|
error = '';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:isOpen onClose={handleClose} title={isTracked ? 'Untrack Player' : 'Track Player'}>
|
<Modal bind:open onClose={handleClose} title={isTracked ? 'Untrack Player' : 'Track Player'}>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<svg
|
<svg
|
||||||
@@ -99,7 +103,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Auth Code Input -->
|
<!-- Auth Code Input (only for tracking, untrack doesn't need auth) -->
|
||||||
|
{#if !isTracked}
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="authCode">
|
<label class="label" for="authCode">
|
||||||
<span class="label-text font-medium">Authentication Code *</span>
|
<span class="label-text font-medium">Authentication Code *</span>
|
||||||
@@ -119,27 +124,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Share Code Input (only for tracking) -->
|
|
||||||
{#if !isTracked}
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label" for="shareCode">
|
|
||||||
<span class="label-text font-medium">Share Code (Optional)</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="shareCode"
|
|
||||||
type="text"
|
|
||||||
placeholder="CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
bind:value={shareCode}
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
<div class="label">
|
|
||||||
<span class="label-text-alt text-base-content/60">
|
|
||||||
Optional: Provide a share code if you have no matches yet
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Error Message -->
|
<!-- Error Message -->
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
class?: string;
|
class?: string;
|
||||||
onclick?: () => void;
|
onclick?: () => void;
|
||||||
|
target?: string;
|
||||||
|
rel?: string;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,6 +22,8 @@
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
class: className = '',
|
class: className = '',
|
||||||
onclick,
|
onclick,
|
||||||
|
target,
|
||||||
|
rel,
|
||||||
children
|
children
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
@@ -46,7 +50,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if href}
|
{#if href}
|
||||||
<a {href} class={classes} aria-disabled={disabled}>
|
<a {href} {target} {rel} class={classes} aria-disabled={disabled}>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface UserPreferences {
|
|||||||
theme: 'cs2dark' | 'cs2light' | 'auto';
|
theme: 'cs2dark' | 'cs2light' | 'auto';
|
||||||
language: string;
|
language: string;
|
||||||
favoriteMap?: string;
|
favoriteMap?: string;
|
||||||
favoritePlayers: number[];
|
favoritePlayers: string[]; // Steam IDs as strings to preserve uint64 precision
|
||||||
showAdvancedStats: boolean;
|
showAdvancedStats: boolean;
|
||||||
dateFormat: 'relative' | 'absolute';
|
dateFormat: 'relative' | 'absolute';
|
||||||
timezone: string;
|
timezone: string;
|
||||||
@@ -76,13 +76,13 @@ const createPreferencesStore = () => {
|
|||||||
setLanguage: (language: string) => {
|
setLanguage: (language: string) => {
|
||||||
update((prefs) => ({ ...prefs, language }));
|
update((prefs) => ({ ...prefs, language }));
|
||||||
},
|
},
|
||||||
addFavoritePlayer: (playerId: number) => {
|
addFavoritePlayer: (playerId: string) => {
|
||||||
update((prefs) => ({
|
update((prefs) => ({
|
||||||
...prefs,
|
...prefs,
|
||||||
favoritePlayers: [...new Set([...prefs.favoritePlayers, playerId])]
|
favoritePlayers: [...new Set([...prefs.favoritePlayers, playerId])]
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
removeFavoritePlayer: (playerId: number) => {
|
removeFavoritePlayer: (playerId: string) => {
|
||||||
update((prefs) => ({
|
update((prefs) => ({
|
||||||
...prefs,
|
...prefs,
|
||||||
favoritePlayers: prefs.favoritePlayers.filter((id) => id !== playerId)
|
favoritePlayers: prefs.favoritePlayers.filter((id) => id !== playerId)
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ export interface PlayerMatch extends Match {
|
|||||||
* Lightweight player metadata for quick previews
|
* Lightweight player metadata for quick previews
|
||||||
*/
|
*/
|
||||||
export interface PlayerMeta {
|
export interface PlayerMeta {
|
||||||
id: number;
|
/** Steam ID (string to preserve uint64 precision, consistent with Player) */
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
recent_matches: number;
|
recent_matches: number;
|
||||||
@@ -81,6 +82,16 @@ export interface PlayerMeta {
|
|||||||
avg_deaths: number;
|
avg_deaths: number;
|
||||||
avg_kast: number;
|
avg_kast: number;
|
||||||
win_rate: number;
|
win_rate: number;
|
||||||
|
/** Number of VAC bans on record (optional) */
|
||||||
|
vac_count?: number;
|
||||||
|
/** Date of last VAC ban (ISO 8601, optional) */
|
||||||
|
vac_date?: string | null;
|
||||||
|
/** Number of game bans on record (optional) */
|
||||||
|
game_ban_count?: number;
|
||||||
|
/** Date of last game ban (ISO 8601, optional) */
|
||||||
|
game_ban_date?: string | null;
|
||||||
|
/** Whether this player is being tracked for automatic match updates (optional) */
|
||||||
|
tracked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const mockPlayers: Player[] = [
|
|||||||
|
|
||||||
/** Mock player metadata */
|
/** Mock player metadata */
|
||||||
export const mockPlayerMeta: PlayerMeta = {
|
export const mockPlayerMeta: PlayerMeta = {
|
||||||
id: 765611980123456,
|
id: '765611980123456',
|
||||||
name: 'TestPlayer1',
|
name: 'TestPlayer1',
|
||||||
avatar:
|
avatar:
|
||||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/fe/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg',
|
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/fe/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg',
|
||||||
@@ -44,7 +44,10 @@ export const mockPlayerMeta: PlayerMeta = {
|
|||||||
avg_kills: 21.3,
|
avg_kills: 21.3,
|
||||||
avg_deaths: 17.8,
|
avg_deaths: 17.8,
|
||||||
avg_kast: 75.2,
|
avg_kast: 75.2,
|
||||||
win_rate: 56.5
|
win_rate: 56.5,
|
||||||
|
vac_count: 0,
|
||||||
|
game_ban_count: 0,
|
||||||
|
tracked: false
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Mock match players */
|
/** Mock match players */
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
import Badge from '$lib/components/ui/Badge.svelte';
|
||||||
import MatchCard from '$lib/components/match/MatchCard.svelte';
|
import MatchCard from '$lib/components/match/MatchCard.svelte';
|
||||||
import RecentPlayers from '$lib/components/player/RecentPlayers.svelte';
|
import RecentPlayers from '$lib/components/player/RecentPlayers.svelte';
|
||||||
|
import PieChart from '$lib/components/charts/PieChart.svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
// Get data from page loader
|
// Get data from page loader
|
||||||
@@ -12,6 +13,39 @@
|
|||||||
|
|
||||||
// Use matches directly - already transformed by API client
|
// Use matches directly - already transformed by API client
|
||||||
const featuredMatches = data.featuredMatches;
|
const featuredMatches = data.featuredMatches;
|
||||||
|
const mapStats = data.mapStats;
|
||||||
|
|
||||||
|
// Count matches being processed (demos not yet parsed)
|
||||||
|
const processingCount = $derived(featuredMatches.filter((m) => !m.demo_parsed).length);
|
||||||
|
|
||||||
|
// Prepare map chart data
|
||||||
|
const mapChartData = $derived({
|
||||||
|
labels: mapStats.map((s) => s.map),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: mapStats.map((s) => s.count),
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(59, 130, 246, 0.8)', // blue
|
||||||
|
'rgba(16, 185, 129, 0.8)', // green
|
||||||
|
'rgba(245, 158, 11, 0.8)', // amber
|
||||||
|
'rgba(239, 68, 68, 0.8)', // red
|
||||||
|
'rgba(139, 92, 246, 0.8)', // purple
|
||||||
|
'rgba(236, 72, 153, 0.8)', // pink
|
||||||
|
'rgba(20, 184, 166, 0.8)' // teal
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgba(255, 255, 255, 0.8)',
|
||||||
|
'rgba(255, 255, 255, 0.8)',
|
||||||
|
'rgba(255, 255, 255, 0.8)',
|
||||||
|
'rgba(255, 255, 255, 0.8)',
|
||||||
|
'rgba(255, 255, 255, 0.8)',
|
||||||
|
'rgba(255, 255, 255, 0.8)',
|
||||||
|
'rgba(255, 255, 255, 0.8)'
|
||||||
|
],
|
||||||
|
borderWidth: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{ icon: Users, label: 'Players Tracked', value: '1.2M+' },
|
{ icon: Users, label: 'Players Tracked', value: '1.2M+' },
|
||||||
@@ -161,9 +195,22 @@
|
|||||||
<!-- Featured Matches -->
|
<!-- Featured Matches -->
|
||||||
<section class="py-16">
|
<section class="py-16">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="mb-8 flex items-center justify-between">
|
<div class="mb-8 flex flex-col items-start justify-between gap-4 md:flex-row md:items-center">
|
||||||
<div>
|
<div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<h2 class="text-3xl font-bold text-base-content">Featured Matches</h2>
|
<h2 class="text-3xl font-bold text-base-content">Featured Matches</h2>
|
||||||
|
{#if processingCount > 0}
|
||||||
|
<Badge variant="warning" size="sm">
|
||||||
|
<span class="relative flex h-2 w-2">
|
||||||
|
<span
|
||||||
|
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-warning opacity-75"
|
||||||
|
></span>
|
||||||
|
<span class="relative inline-flex h-2 w-2 rounded-full bg-warning"></span>
|
||||||
|
</span>
|
||||||
|
<span class="ml-1.5">{processingCount} Processing</span>
|
||||||
|
</Badge>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<p class="mt-2 text-base-content/60">Latest competitive matches from our community</p>
|
<p class="mt-2 text-base-content/60">Latest competitive matches from our community</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" href="/matches">View All</Button>
|
<Button variant="ghost" href="/matches">View All</Button>
|
||||||
@@ -238,6 +285,93 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Statistics Dashboard -->
|
||||||
|
{#if mapStats.length > 0}
|
||||||
|
<section class="border-t border-base-300 bg-base-100 py-16">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<h2 class="text-3xl font-bold text-base-content">Community Statistics</h2>
|
||||||
|
<p class="mt-2 text-base-content/60">
|
||||||
|
Insights from {data.totalMatchesAnalyzed.toLocaleString()} recent matches
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-8 lg:grid-cols-2">
|
||||||
|
<!-- Most Played Maps -->
|
||||||
|
<Card padding="lg">
|
||||||
|
<h3 class="mb-6 text-xl font-semibold text-base-content">Most Played Maps</h3>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
<PieChart data={mapChartData} options={{ maintainAspectRatio: true }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 space-y-2">
|
||||||
|
{#each mapStats as stat, i}
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="h-3 w-3 rounded-sm"
|
||||||
|
style="background-color: {mapChartData.datasets[0]?.backgroundColor?.[i] ||
|
||||||
|
'rgba(59, 130, 246, 0.8)'}"
|
||||||
|
></div>
|
||||||
|
<span class="text-sm font-medium text-base-content">{stat.map}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-base-content/60"
|
||||||
|
>{stat.count} matches ({((stat.count / data.totalMatchesAnalyzed) * 100).toFixed(
|
||||||
|
1
|
||||||
|
)}%)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Quick Stats Summary -->
|
||||||
|
<Card padding="lg">
|
||||||
|
<h3 class="mb-6 text-xl font-semibold text-base-content">Recent Activity</h3>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="rounded-lg bg-base-200 p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-base-content/60">Total Matches</p>
|
||||||
|
<p class="text-3xl font-bold text-primary">
|
||||||
|
{data.totalMatchesAnalyzed.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<TrendingUp class="h-12 w-12 text-primary/40" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-base-content/50">From the last 24 hours</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-base-200 p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-base-content/60">Most Popular Map</p>
|
||||||
|
<p class="text-3xl font-bold text-secondary">
|
||||||
|
{mapStats[0]?.map || 'N/A'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="success" size="lg"
|
||||||
|
>{mapStats[0]
|
||||||
|
? `${((mapStats[0].count / data.totalMatchesAnalyzed) * 100).toFixed(0)}%`
|
||||||
|
: '0%'}</Badge
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-base-content/50">
|
||||||
|
Played in {mapStats[0]?.count || 0} matches
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<Button variant="ghost" href="/matches">View All Match Statistics →</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Features Section -->
|
<!-- Features Section -->
|
||||||
<section class="border-t border-base-300 bg-base-200 py-16">
|
<section class="border-t border-base-300 bg-base-200 py-16">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
|
|||||||
@@ -10,11 +10,27 @@ export const load: PageLoad = async ({ parent }) => {
|
|||||||
await parent();
|
await parent();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load featured matches for homepage carousel
|
// Load matches for homepage - get more for statistics
|
||||||
const matchesData = await api.matches.getMatches({ limit: 9 });
|
const matchesData = await api.matches.getMatches({ limit: 50 });
|
||||||
|
const allMatches = matchesData.matches;
|
||||||
|
|
||||||
|
// Calculate map statistics
|
||||||
|
const mapCounts = new Map<string, number>();
|
||||||
|
allMatches.forEach((match) => {
|
||||||
|
const count = mapCounts.get(match.map) || 0;
|
||||||
|
mapCounts.set(match.map, count + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to sorted array for pie chart
|
||||||
|
const mapStats = Array.from(mapCounts.entries())
|
||||||
|
.map(([map, count]) => ({ map, count }))
|
||||||
|
.sort((a, b) => b.count - a.count)
|
||||||
|
.slice(0, 7); // Top 7 maps
|
||||||
|
|
||||||
return {
|
return {
|
||||||
featuredMatches: matchesData.matches.slice(0, 9), // Get 9 matches for carousel (3 slides)
|
featuredMatches: allMatches.slice(0, 9), // Get 9 matches for carousel (3 slides)
|
||||||
|
mapStats, // For most played maps pie chart
|
||||||
|
totalMatchesAnalyzed: allMatches.length,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'CS2.WTF - Statistics for CS2 Matchmaking',
|
title: 'CS2.WTF - Statistics for CS2 Matchmaking',
|
||||||
description:
|
description:
|
||||||
@@ -31,6 +47,8 @@ export const load: PageLoad = async ({ parent }) => {
|
|||||||
// Return empty data - page will show without featured matches
|
// Return empty data - page will show without featured matches
|
||||||
return {
|
return {
|
||||||
featuredMatches: [],
|
featuredMatches: [],
|
||||||
|
mapStats: [],
|
||||||
|
totalMatchesAnalyzed: 0,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'CS2.WTF - Statistics for CS2 Matchmaking',
|
title: 'CS2.WTF - Statistics for CS2 Matchmaking',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -15,11 +15,10 @@
|
|||||||
|
|
||||||
import { error, json } from '@sveltejs/kit';
|
import { error, json } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { env } from '$env/dynamic/private';
|
|
||||||
|
|
||||||
// Get backend API URL from environment variable
|
// Get backend API URL from environment variable
|
||||||
// Note: We use $env/dynamic/private instead of import.meta.env for server-side access
|
// Use import.meta.env for Vite environment variables (works in all environments)
|
||||||
const API_BASE_URL = env.VITE_API_BASE_URL || 'https://api.csgow.tf';
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.csgow.tf';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET request handler
|
* GET request handler
|
||||||
|
|||||||
@@ -41,6 +41,17 @@
|
|||||||
const img = event.target as HTMLImageElement;
|
const img = event.target as HTMLImageElement;
|
||||||
img.src = '/images/map_screenshots/default.webp';
|
img.src = '/images/map_screenshots/default.webp';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDownloadDemo() {
|
||||||
|
if (!match.share_code) {
|
||||||
|
alert('Share code not available for this match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Open the demo download URL (typically from Valve servers or cached location)
|
||||||
|
// Format: steam://rungame/730/76561202255233023/+csgo_download_match%20{SHARE_CODE}
|
||||||
|
const downloadUrl = `steam://rungame/730/76561202255233023/+csgo_download_match%20${match.share_code}`;
|
||||||
|
window.location.href = downloadUrl;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Match Header with Background -->
|
<!-- Match Header with Background -->
|
||||||
@@ -72,9 +83,11 @@
|
|||||||
{mapName}
|
{mapName}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
{#if match.demo_parsed}
|
{#if match.demo_parsed && match.share_code}
|
||||||
<button
|
<button
|
||||||
|
onclick={handleDownloadDemo}
|
||||||
class="btn btn-ghost gap-2 border border-white/25 bg-white/15 text-white backdrop-blur-md hover:bg-white/25"
|
class="btn btn-ghost gap-2 border border-white/25 bg-white/15 text-white backdrop-blur-md hover:bg-white/25"
|
||||||
|
title="Download this match demo to your Steam client"
|
||||||
>
|
>
|
||||||
<Download class="h-4 w-4" />
|
<Download class="h-4 w-4" />
|
||||||
<span class="hidden sm:inline">Download Demo</span>
|
<span class="hidden sm:inline">Download Demo</span>
|
||||||
|
|||||||
@@ -240,7 +240,7 @@
|
|||||||
|
|
||||||
<!-- Round Timeline -->
|
<!-- Round Timeline -->
|
||||||
{#if rounds && rounds.rounds && rounds.rounds.length > 0}
|
{#if rounds && rounds.rounds && rounds.rounds.length > 0}
|
||||||
<RoundTimeline rounds={rounds.rounds} />
|
<RoundTimeline rounds={rounds.rounds} maxRounds={match.max_rounds} />
|
||||||
{:else}
|
{:else}
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|||||||
@@ -53,22 +53,23 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Prepare data table columns
|
// Prepare data table columns
|
||||||
|
type PlayerWithStats = (typeof playersWithStats)[0];
|
||||||
const detailsColumns = [
|
const detailsColumns = [
|
||||||
{
|
{
|
||||||
key: 'avatar' as keyof (typeof playersWithStats)[0],
|
key: 'avatar' as keyof PlayerWithStats,
|
||||||
label: '',
|
label: '',
|
||||||
sortable: false,
|
sortable: false,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (value: string | number | boolean | undefined, row: (typeof playersWithStats)[0]) => {
|
render: (_value: unknown, row: PlayerWithStats) => {
|
||||||
const avatarUrl = row.avatar || '';
|
const avatarUrl = row.avatar || '';
|
||||||
return `<img src="${avatarUrl}" alt="${row.name}" class="h-10 w-10 rounded-full border-2 border-base-300" />`;
|
return `<img src="${avatarUrl}" alt="${row.name}" class="h-10 w-10 rounded-full border-2 border-base-300" />`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'name' as keyof (typeof playersWithStats)[0],
|
key: 'name' as keyof PlayerWithStats,
|
||||||
label: 'Player',
|
label: 'Player',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (value: string | number | boolean | undefined, row: (typeof playersWithStats)[0]) => {
|
render: (value: unknown, row: PlayerWithStats) => {
|
||||||
const strValue = value !== undefined ? String(value) : '';
|
const strValue = value !== undefined ? String(value) : '';
|
||||||
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
|
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
|
||||||
// Color indicator dot
|
// Color indicator dot
|
||||||
@@ -108,40 +109,36 @@
|
|||||||
class: 'font-mono'
|
class: 'font-mono'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'kd' as keyof (typeof playersWithStats)[0],
|
key: 'kd' as keyof PlayerWithStats,
|
||||||
label: 'K/D',
|
label: 'K/D',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono',
|
class: 'font-mono',
|
||||||
format: (v: string | number | undefined, _row: (typeof playersWithStats)[0]) =>
|
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(2) : '0.00')
|
||||||
v !== undefined ? (v as number).toFixed(2) : '0.00'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'adr' as keyof (typeof playersWithStats)[0],
|
key: 'adr' as keyof PlayerWithStats,
|
||||||
label: 'ADR',
|
label: 'ADR',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono',
|
class: 'font-mono',
|
||||||
format: (v: string | number | undefined, _row: (typeof playersWithStats)[0]) =>
|
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '0.0')
|
||||||
v !== undefined ? (v as number).toFixed(1) : '0.0'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'hsPercent' as keyof (typeof playersWithStats)[0],
|
key: 'hsPercent' as keyof PlayerWithStats,
|
||||||
label: 'HS%',
|
label: 'HS%',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono',
|
class: 'font-mono',
|
||||||
format: (v: string | number | undefined, _row: (typeof playersWithStats)[0]) =>
|
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '0.0')
|
||||||
v !== undefined ? (v as number).toFixed(1) : '0.0'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'kast' as keyof (typeof playersWithStats)[0],
|
key: 'kast' as keyof PlayerWithStats,
|
||||||
label: 'KAST%',
|
label: 'KAST%',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono',
|
class: 'font-mono',
|
||||||
format: (v: string | number | undefined, _row: (typeof playersWithStats)[0]) =>
|
format: (v: unknown) => (v !== undefined ? (v as number).toFixed(1) : '-')
|
||||||
v !== undefined ? (v as number).toFixed(1) : '-'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'mvp' as keyof (typeof playersWithStats)[0],
|
key: 'mvp' as keyof (typeof playersWithStats)[0],
|
||||||
|
|||||||
@@ -27,6 +27,11 @@
|
|||||||
const tTeamId = 2;
|
const tTeamId = 2;
|
||||||
const ctTeamId = 3;
|
const ctTeamId = 3;
|
||||||
|
|
||||||
|
// Calculate halftime round based on max_rounds
|
||||||
|
// MR12 (24 rounds): halftime after round 12
|
||||||
|
// MR15 (30 rounds): halftime after round 15
|
||||||
|
const halfPoint = match.max_rounds === 30 ? 15 : 12;
|
||||||
|
|
||||||
// Only process if rounds data exists
|
// Only process if rounds data exists
|
||||||
let teamEconomy = $state<TeamEconomy[]>([]);
|
let teamEconomy = $state<TeamEconomy[]>([]);
|
||||||
let equipmentChartData = $state<{
|
let equipmentChartData = $state<{
|
||||||
@@ -94,7 +99,7 @@
|
|||||||
const ct_totalEconomy = ct_bank + ct_spent;
|
const ct_totalEconomy = ct_bank + ct_spent;
|
||||||
|
|
||||||
// Determine perspective based on round (teams swap at half)
|
// Determine perspective based on round (teams swap at half)
|
||||||
const halfPoint = 12; // MR12 format: rounds 1-12 first half, 13-24 second half
|
// halfPoint is calculated above based on match.max_rounds
|
||||||
let economyAdvantage;
|
let economyAdvantage;
|
||||||
if (roundData.round <= halfPoint) {
|
if (roundData.round <= halfPoint) {
|
||||||
// First half: T - CT
|
// First half: T - CT
|
||||||
@@ -153,6 +158,7 @@
|
|||||||
data: teamEconomy.map((r) => (r.economyAdvantage > 0 ? r.economyAdvantage : 0)),
|
data: teamEconomy.map((r) => (r.economyAdvantage > 0 ? r.economyAdvantage : 0)),
|
||||||
borderColor: 'rgb(59, 130, 246)',
|
borderColor: 'rgb(59, 130, 246)',
|
||||||
backgroundColor: 'rgba(59, 130, 246, 0.6)',
|
backgroundColor: 'rgba(59, 130, 246, 0.6)',
|
||||||
|
// @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid
|
||||||
fill: 'origin',
|
fill: 'origin',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
@@ -163,6 +169,7 @@
|
|||||||
data: teamEconomy.map((r) => (r.economyAdvantage < 0 ? r.economyAdvantage : 0)),
|
data: teamEconomy.map((r) => (r.economyAdvantage < 0 ? r.economyAdvantage : 0)),
|
||||||
borderColor: 'rgb(249, 115, 22)',
|
borderColor: 'rgb(249, 115, 22)',
|
||||||
backgroundColor: 'rgba(249, 115, 22, 0.6)',
|
backgroundColor: 'rgba(249, 115, 22, 0.6)',
|
||||||
|
// @ts-expect-error - Chart.js types incorrectly show fill as boolean, but 'origin' is valid
|
||||||
fill: 'origin',
|
fill: 'origin',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
|
|||||||
@@ -55,15 +55,13 @@
|
|||||||
const sortedPlayerWeapons = playerWeaponsData.sort((a, b) => b.total_kills - a.total_kills);
|
const sortedPlayerWeapons = playerWeaponsData.sort((a, b) => b.total_kills - a.total_kills);
|
||||||
|
|
||||||
// Prepare data table columns
|
// Prepare data table columns
|
||||||
|
type PlayerWeapon = (typeof sortedPlayerWeapons)[0];
|
||||||
const weaponColumns = [
|
const weaponColumns = [
|
||||||
{
|
{
|
||||||
key: 'player_name' as const,
|
key: 'player_name' as const,
|
||||||
label: 'Player',
|
label: 'Player',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (
|
render: (value: unknown, row: PlayerWeapon) => {
|
||||||
value: string | number | boolean | undefined,
|
|
||||||
row: (typeof sortedPlayerWeapons)[0]
|
|
||||||
) => {
|
|
||||||
const strValue = value !== undefined ? String(value) : '';
|
const strValue = value !== undefined ? String(value) : '';
|
||||||
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
|
const teamClass = row.team_id === firstTeamId ? 'text-terrorist' : 'text-ct';
|
||||||
return `<a href="/player/${row.player_id}" class="font-medium hover:underline ${teamClass}">${strValue}</a>`;
|
return `<a href="/player/${row.player_id}" class="font-medium hover:underline ${teamClass}">${strValue}</a>`;
|
||||||
@@ -89,8 +87,7 @@
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
class: 'font-mono',
|
class: 'font-mono',
|
||||||
format: (v: string | number | undefined) =>
|
format: (v: unknown) => (v !== undefined ? (v as number).toLocaleString() : '0')
|
||||||
v !== undefined ? (v as number).toLocaleString() : '0'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'total_hits' as const,
|
key: 'total_hits' as const,
|
||||||
|
|||||||
@@ -476,9 +476,18 @@
|
|||||||
<input
|
<input
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by player name, match ID, or share code..."
|
placeholder="Search by match ID or share code..."
|
||||||
class="input input-bordered w-full pl-10"
|
class="input input-bordered w-full pl-10"
|
||||||
|
title="Player name search coming soon when API supports it"
|
||||||
/>
|
/>
|
||||||
|
<div class="absolute right-3 top-1/2 -translate-y-1/2">
|
||||||
|
<div
|
||||||
|
class="tooltip tooltip-left"
|
||||||
|
data-tip="Player name search will be available when the API supports it"
|
||||||
|
>
|
||||||
|
<Badge variant="warning" size="sm">Player Search Coming Soon</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" variant="primary">
|
<Button type="submit" variant="primary">
|
||||||
@@ -723,7 +732,7 @@
|
|||||||
<div class="mt-4 flex flex-wrap items-center gap-2 border-t border-base-300 pt-4">
|
<div class="mt-4 flex flex-wrap items-center gap-2 border-t border-base-300 pt-4">
|
||||||
<span class="text-sm font-medium text-base-content/70">Active Filters:</span>
|
<span class="text-sm font-medium text-base-content/70">Active Filters:</span>
|
||||||
{#if currentSearch}
|
{#if currentSearch}
|
||||||
<Badge variant="info">Search: {currentSearch}</Badge>
|
<Badge variant="info">Match/Share Code: {currentSearch}</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
{#if currentMap}
|
{#if currentMap}
|
||||||
<Badge variant="info">Map: {currentMap}</Badge>
|
<Badge variant="info">Map: {currentMap}</Badge>
|
||||||
|
|||||||
@@ -295,7 +295,7 @@
|
|||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant={profile.tracked ? 'success' : 'primary'}
|
variant={profile.tracked ? 'secondary' : 'primary'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={() => (isTrackModalOpen = true)}
|
onclick={() => (isTrackModalOpen = true)}
|
||||||
>
|
>
|
||||||
@@ -324,7 +324,7 @@
|
|||||||
playerId={profile.id}
|
playerId={profile.id}
|
||||||
playerName={profile.name}
|
playerName={profile.name}
|
||||||
isTracked={profile.tracked || false}
|
isTracked={profile.tracked || false}
|
||||||
bind:isOpen={isTrackModalOpen}
|
bind:open={isTrackModalOpen}
|
||||||
ontracked={handleTracked}
|
ontracked={handleTracked}
|
||||||
onuntracked={handleUntracked}
|
onuntracked={handleUntracked}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user