Implement Phase 2: Player search, stats, matches, charts, mode breakdown#26
Implement Phase 2: Player search, stats, matches, charts, mode breakdown#26
Conversation
There was a problem hiding this comment.
Pull request overview
Implements Phase 2 of the player experience: search → navigate to player page → load lifetime stats, charts, match history, and per-mode breakdown, backed by new Go API services/handlers and a CoD API client reliability pass.
Changes:
- Frontend: adds Home search flow w/ recent searches, Player view with mode tabs, stat cards/grids, ECharts visualizations, and match history table.
- Backend: implements
/api/v1/players/search,/stats, and/matcheswith services + persistence (players, match snapshots, stats snapshots). - CoD API client: fixes redirect/token-expiry handling and enriches stats parsing with per-mode breakdown.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/views/PlayerView.vue | Player page data loading, mode tabs, stats/charts rendering, error display, skeletons. |
| web/src/views/HomeView.vue | Search form wired to backend search + recent searches via localStorage. |
| web/src/utils/modes.ts | Defines top-level modes + mode label mapping helper for UI. |
| web/src/utils/format.ts | Shared number/KD/percent/time formatting utilities. |
| web/src/utils/echarts.ts | Tree-shaken ECharts setup + shared dark theme/colors export. |
| web/src/types/index.ts | Expands types for mode breakdown, search result, paginated matches. |
| web/src/stores/index.ts | Pinia player store: search/stats/matches fetching w/ separate loading/error state. |
| web/src/components/charts/RadarChart.vue | Radar chart visualization for key derived stats. |
| web/src/components/charts/PlacementChart.vue | Donut chart for placement distribution. |
| web/src/components/charts/PerformanceTrend.vue | Line chart computing average K/D trend from matches. |
| web/src/components/charts/ModeComparisonChart.vue | Bar chart comparing kills/deaths across mode breakdown entries. |
| web/src/components/charts/KDGauge.vue | Gauge chart for K/D ratio. |
| web/src/components/StatsHero.vue | Hero stat cards (K/D, wins, kills). |
| web/src/components/StatsGrid.vue | Secondary stats grid (12 cards) with responsive layout. |
| web/src/components/StatCard.vue | Reusable stat card component with size + K/D color classes. |
| web/src/components/RankedBadge.vue | Small badge/tag for ranked division/SR display. |
| web/src/components/ModeSummaryRow.vue | Horizontal scrollable per-mode summary cards emitting mode selection. |
| web/src/components/MatchHistory.vue | Naive UI data table for matches with sorting/filtering/win highlighting. |
| web/src/components/ChartsSection.vue | Layout wrapper for chart cards + empty states. |
| internal/service/player.go | Player search + stats service logic w/ upsert + stats snapshot save. |
| internal/service/match.go | Match fetching/persistence + paginated response and DB fallback behavior. |
| internal/router/router.go | Wires new player/match handlers into /api/v1/players/* routes. |
| internal/repository/match_repo.go | Adds match count query used for pagination metadata. |
| internal/model/player.go | Makes ActivisionID nullable pointer to match DB nullable column. |
| internal/handler/player.go | Implements HTTP handlers for search + stats endpoints. |
| internal/handler/match.go | Implements HTTP handler for matches endpoint w/ limit/offset parsing. |
| internal/handler/errors.go | Centralizes sentinel error → HTTP JSON mapping with logging. |
| internal/codclient/types.go | Adds modeBreakdown types + fixes Level/Prestige float decoding. |
| internal/codclient/client.go | Adds redirect/token-expiry detection + parses 200/{"status":"error"} responses and mode breakdown. |
| docker-compose.yml | Changes Postgres host port mapping to 5435. |
| cmd/server/main.go | Wires repos/services/handlers into server initialization. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
web/src/components/MatchHistory.vue
Outdated
| return d.toLocaleDateString('en-US', { | ||
| month: 'short', | ||
| day: 'numeric', | ||
| hour: '2-digit', | ||
| minute: '2-digit', |
There was a problem hiding this comment.
formatDateTime uses Date.prototype.toLocaleDateString while passing hour/minute options. In most runtimes those time options are ignored for toLocaleDateString, so the column may show only the date and drop the time. Use toLocaleString (or an Intl.DateTimeFormat) when you need both date and time.
web/src/components/MatchHistory.vue
Outdated
| filterOptions: computed(() => { | ||
| const modes = [...new Set(props.matches.map((m) => m.mode))] | ||
| return modes.map((m) => ({ label: getModeLabel(m), value: m })) | ||
| }).value, | ||
| }, |
There was a problem hiding this comment.
filterOptions is computed from props.matches but you immediately read .value while defining the columns array, which freezes the options at setup time. When matches load/update, the filter options won't refresh. Make columns (or just filterOptions) reactive so it recomputes when props.matches changes.
| if upsertErr := s.matchRepo.UpsertBatch(ctx, player.ID, modelMatches); upsertErr != nil { | ||
| slog.Warn("failed to persist matches", "error", upsertErr) | ||
| } |
There was a problem hiding this comment.
matches.match_id is globally UNIQUE in the current schema (migrations/000001_init.up.sql), but UpsertBatch uses ON CONFLICT (match_id) DO NOTHING. If two different players share a match (same match_id) and you search them both, the second player's rows will not be inserted, and the DB query (filtered by player_id) will return empty/partial results even though the API call succeeded. Consider changing uniqueness to (match_id, player_id) (or introducing a join table) and updating the upsert conflict target accordingly.
| ports: | ||
| - "5432:5432" | ||
| - "5435:5432" | ||
| environment: |
There was a problem hiding this comment.
docker-compose maps Postgres to host port 5435 now, but .env.example (and likely local dev docs) still reference localhost:5432. This will cause connection failures for anyone using the example env as-is; update the example/config or keep the default port mapping consistent.
6818187 to
65747a3
Compare
…ode breakdown (#9-#13) Backend: - PlayerService/Handler for search and stats endpoints - MatchService/Handler for match history with pagination - Multi-title support (mw/wz, mw2/wz2, mw/mp, mw2/mp) via title/mode query params - CoD API error handling: redirect detection, JSON error body parsing, HTML detection - ModeBreakdown parsing from lifetime stats response - Fix profileResponse Level/Prestige types (float64), nullable ActivisionID (*string) - Base URL changed to www.callofduty.com (my.callofduty.com redirects to store) Frontend: - Search-then-navigate flow with error alerts and recent searches (localStorage) - Game selector (Warzone, WZ2, MW MP, MW2 MP) and platform selector - StatCard, StatsHero, StatsGrid components with responsive layouts - 5 ECharts visualizations: Radar, K/D Gauge, Mode Comparison, Placement, Trend - MatchHistory with NDataTable sorting, mode filter, win highlighting, gulag badges - Mode tabs (NTabs segment) with ModeSummaryRow click-to-switch - Split loading/error state (statsLoading/matchesLoading) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
65747a3 to
198cdbd
Compare
|
Addressed all 4 Copilot review comments in the latest force push (198cdbd):
|
grovecj
left a comment
There was a problem hiding this comment.
Addressed all 4 Copilot review items:
- toLocaleDateString → toLocaleString — Fixed so hour/minute render correctly
- filterOptions reactivity —
columnsandfilterOptionsare now bothcomputed()refs - match_id uniqueness — Changed to composite
UNIQUE(match_id, player_id)in migration + repo - docker-compose port — Reverted to standard
5432:5432(no diff since it matches the base branch now)
| month: 'short', | ||
| day: 'numeric', | ||
| hour: '2-digit', | ||
| minute: '2-digit', |
There was a problem hiding this comment.
Fixed — changed toLocaleDateString to toLocaleString so the hour/minute options are properly rendered.
| }, | ||
| filterOptions: filterOptions.value, | ||
| }, | ||
| { |
There was a problem hiding this comment.
Fixed — extracted filterOptions as a standalone computed() and made columns itself a computed() ref, so both recompute reactively when props.matches changes.
| damage_dealt, damage_taken, gulag_result, match_time, raw_data) | ||
| VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) | ||
| ON CONFLICT (match_id) DO NOTHING | ||
| ON CONFLICT (match_id, player_id) DO NOTHING |
There was a problem hiding this comment.
Fixed — changed the migration from match_id UNIQUE to a composite UNIQUE(match_id, player_id) index, and updated the ON CONFLICT target here to (match_id, player_id). Two players sharing a match now get separate rows.
Summary
GET /api/v1/players/search, frontend search-then-navigate flow with error alerts and recent searches in localStorageGET /api/v1/players/{platform}/{gamertag}/matches, NDataTable with sorting, mode filter, win highlighting, and gulag badgesBugfixes (commit 2)
my.callofduty.com→www.callofduty.comredirects to store){"status":"error"}JSON bodies into sentinel errorsprofileResponse.Level/Prestigetype mismatch (API returns float, struct expected int)activision_idcolumn scan crash (string→*string)Multi-title support (commit 3)
titleandmodequery params to all API endpoints (defaults:mw/wz)mw/wz), Warzone 2 (mw2/wz2), MW MP (mw/mp), MW2 MP (mw2/mp)idlookup type foruno(Activision) platform per API specTest plan
go build ./...andgo vet ./...passcd web && npm run build-only && npm run type-checkpasscd web && npx oxlint . && npx eslint . --cachepassCOD_SSO_TOKEN+ PostgreSQL🤖 Generated with Claude Code