Skip to content

Commit 8dc5229

Browse files
grovecjclaude
andauthored
Add demo seed data and API fallback for portfolio mode (#27)
## Summary - **Seed script** (`migrations/seed.sql`): 5 demo players at varied skill levels (Elite→Casual), full lifetime stats with mode breakdowns, and 125 matches with realistic placements/K-D/gulag results - **DB fallback** (`internal/service/player.go`): `SearchPlayer` and `GetPlayerStats` now fall back to the database when the CoD API is unavailable, serving seeded data transparently - **Demo banner** (`web/src/App.vue`): Dismissible warning alert explaining data is generated (persists per session via sessionStorage) - **Demo player links** (`web/src/views/HomeView.vue`): Clickable buttons on the home page for all 5 demo players so visitors know what to search ### Context The Activision stats API (`papi-client`) was fully shut down in April 2024. This change converts the app into a portfolio-ready demo that showcases the full stack without requiring a live API. ## Test plan - [ ] Run `psql $DATABASE_URL -f migrations/seed.sql` to seed the database - [ ] `go build ./...` and `go vet ./...` pass - [ ] `cd web && npm run build-only && npm run type-check` pass - [ ] Click any demo player on HomeView — verify redirect to PlayerView with stats - [ ] Verify stats cards, charts, and match history table populate correctly - [ ] Verify the demo banner appears and can be dismissed - [ ] Search for a non-existent player — verify "Player not found" error 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5f3a12e commit 8dc5229

4 files changed

Lines changed: 481 additions & 4 deletions

File tree

internal/service/player.go

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func NewPlayerService(codClient codclient.CodClient, playerRepo *repository.Play
2929
}
3030

3131
// SearchPlayer verifies a player exists via the CoD API, persists them, and returns search results.
32+
// Falls back to database if the API is unavailable.
3233
func (s *PlayerService) SearchPlayer(ctx context.Context, platform, gamertag, title, mode string) (*PlayerSearchResult, error) {
3334
if title == "" {
3435
title = "mw"
@@ -39,7 +40,8 @@ func (s *PlayerService) SearchPlayer(ctx context.Context, platform, gamertag, ti
3940

4041
stats, err := s.codClient.GetPlayerStats(ctx, platform, gamertag, title, mode)
4142
if err != nil {
42-
return nil, err
43+
slog.Warn("cod api unavailable, falling back to database", "error", err)
44+
return s.searchFromDB(ctx, platform, gamertag, mode)
4345
}
4446

4547
player, err := s.playerRepo.Upsert(ctx, platform, gamertag)
@@ -64,6 +66,7 @@ func (s *PlayerService) SearchPlayer(ctx context.Context, platform, gamertag, ti
6466
}
6567

6668
// GetPlayerStats fetches player stats from the CoD API, upserts the player, and saves a snapshot.
69+
// Falls back to database if the API is unavailable.
6770
func (s *PlayerService) GetPlayerStats(ctx context.Context, platform, gamertag, title, mode string) (*codclient.PlayerStats, error) {
6871
if title == "" {
6972
title = "mw"
@@ -74,7 +77,8 @@ func (s *PlayerService) GetPlayerStats(ctx context.Context, platform, gamertag,
7477

7578
stats, err := s.codClient.GetPlayerStats(ctx, platform, gamertag, title, mode)
7679
if err != nil {
77-
return nil, err
80+
slog.Warn("cod api unavailable for stats, falling back to database", "error", err)
81+
return s.getStatsFromDB(ctx, platform, gamertag, mode)
7882
}
7983

8084
player, err := s.playerRepo.Upsert(ctx, platform, gamertag)
@@ -92,3 +96,57 @@ func (s *PlayerService) GetPlayerStats(ctx context.Context, platform, gamertag,
9296

9397
return stats, nil
9498
}
99+
100+
// searchFromDB looks up a player and their latest stats from the database.
101+
func (s *PlayerService) searchFromDB(ctx context.Context, platform, gamertag, mode string) (*PlayerSearchResult, error) {
102+
player, err := s.playerRepo.GetByPlatformAndTag(ctx, platform, gamertag)
103+
if err != nil {
104+
return nil, err
105+
}
106+
if player == nil {
107+
return nil, codclient.ErrPlayerNotFound
108+
}
109+
110+
stats, err := s.getStatsFromDB(ctx, platform, gamertag, mode)
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
return &PlayerSearchResult{
116+
PlayerID: player.ID,
117+
Platform: platform,
118+
Gamertag: gamertag,
119+
Stats: stats,
120+
}, nil
121+
}
122+
123+
// getStatsFromDB loads the latest stats snapshot for a player from the database.
124+
func (s *PlayerService) getStatsFromDB(ctx context.Context, platform, gamertag, mode string) (*codclient.PlayerStats, error) {
125+
player, err := s.playerRepo.GetByPlatformAndTag(ctx, platform, gamertag)
126+
if err != nil {
127+
return nil, err
128+
}
129+
if player == nil {
130+
return nil, codclient.ErrPlayerNotFound
131+
}
132+
133+
statsData, _, err := s.playerRepo.GetLatestStats(ctx, player.ID, mode)
134+
if err != nil {
135+
return nil, err
136+
}
137+
if statsData == nil {
138+
return nil, codclient.ErrPlayerNotFound
139+
}
140+
141+
// statsData is any (from pgx JSONB scan) — round-trip through JSON to decode
142+
jsonBytes, err := json.Marshal(statsData)
143+
if err != nil {
144+
return nil, err
145+
}
146+
var stats codclient.PlayerStats
147+
if err := json.Unmarshal(jsonBytes, &stats); err != nil {
148+
return nil, err
149+
}
150+
151+
return &stats, nil
152+
}

0 commit comments

Comments
 (0)