Skip to content

Commit bd89996

Browse files
committed
feat: Complete TASK-006 - Starter Pokemon System with auto-claim
- Backend: /api/starter/claim endpoint creates 3 starters for new users - Starters: Flametail Jr (Fire), Ripplefin (Water), Leaflet (Grass) - Frontend: Auto-claim on first visit if collection is empty - Manual claim button as fallback if auto-claim fails - Added tests for auto-claim functionality - Total: 156 tests passing Auto-claim prevents the 'no Pokemon to start with' UX blocker. New users automatically get 3 Common-type starters upon first load.
1 parent 5a6c22b commit bd89996

4 files changed

Lines changed: 112 additions & 25 deletions

File tree

PROGRESS.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Pokemon App - Progress Log
2+
3+
## 2026-02-12
4+
5+
### 07:17 - TASK-006: Starter Pokemon System ✅ COMPLETED
6+
7+
**What was done:**
8+
- Implemented auto-initialization of starter Pokemon for new users
9+
- Modified Collection.jsx to automatically claim starters when collection is empty
10+
- Added `hasAttemptedAutoClaim` ref to prevent infinite loops
11+
- Backend already had `/api/starter/claim` endpoint with 3 starters:
12+
- Flametail Jr (Fire type, power 25)
13+
- Ripplefin (Water type, power 25)
14+
- Leaflet (Grass type, power 25)
15+
- Added tests for auto-claim behavior (2 tests)
16+
- All 156 tests now passing
17+
18+
**Files modified:**
19+
- `src/pages/Collection.jsx` - Added auto-claim logic
20+
- `src/pages/Collection.test.jsx` - Added auto-claim tests
21+
- `TASKS.md` - Marked TASK-006 complete
22+
23+
**Next priority:** TASK-008: Pokemon Creator Feature [P2 - Medium]

TASKS.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,13 @@
3939

4040
### Active Tasks (CEO Priority Order)
4141

42-
- [ ] **TASK-006: Starter Pokemon System** [P0 - Critical]
43-
- New users start with 3 beginner Pokemon
44-
- Ensure users can always catch (team can't be empty)
45-
- Auto-initialize on first visit if no Pokemon owned
42+
- [x] **TASK-006: Starter Pokemon System** [P0 - Critical] ✅ COMPLETED
43+
- ✅ Backend: `/api/starter/claim` endpoint in worker/index.js
44+
- ✅ Three starter Pokemon defined (Flametail Jr, Ripplefin, Leaflet)
45+
- ✅ API client: `claimStarters()` method
46+
- ✅ Frontend: Auto-claim on first visit if collection is empty
47+
- ✅ Manual claim button as fallback
48+
- ✅ 2 tests added for auto-claim (156 total tests passing)
4649

4750
- [x] **TASK-007: Base44 Data Migration** [P1 - High] ✅ COMPLETED
4851
- ✅ D1 database already seeded with 41 Pokemon

src/pages/Collection.jsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,34 @@ export default function Collection({ onNavigate }) {
2020
const [teamMessage, setTeamMessage] = useState('');
2121
const pokemonCache = useRef(new Map()); // Cache: pokemon_id → pokemon data
2222

23+
// Track if we've attempted auto-claim to prevent loops
24+
const hasAttemptedAutoClaim = useRef(false);
25+
2326
// Initial load: just get the caught list (lightweight, no images)
27+
// Auto-claim starters for new users
2428
useEffect(() => {
2529
(async () => {
2630
try {
2731
const caughtList = await pokemonAPI.getCaughtPokemon();
32+
33+
// Auto-initialize starters for new users (empty collection, first visit)
34+
if (caughtList.length === 0 && !hasAttemptedAutoClaim.current) {
35+
hasAttemptedAutoClaim.current = true;
36+
try {
37+
const result = await pokemonAPI.claimStarters();
38+
if (result.success) {
39+
// Refresh the collection with new starters
40+
const updatedList = await pokemonAPI.getCaughtPokemon();
41+
setAllCaught(updatedList);
42+
setIsLoading(false);
43+
return;
44+
}
45+
} catch (claimErr) {
46+
console.error('Auto-claim failed:', claimErr);
47+
// Continue to normal flow even if auto-claim fails
48+
}
49+
}
50+
2851
setAllCaught(caughtList);
2952
} catch (err) {
3053
console.error("Failed to fetch collection:", err);

src/pages/Collection.test.jsx

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
2-
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
2+
import { render, screen, waitFor } from '@testing-library/react';
33
import Collection from './Collection';
44
import { pokemonAPI } from '@/api/client';
55

@@ -10,6 +10,7 @@ vi.mock('@/api/client', () => ({
1010
getPokemon: vi.fn(),
1111
updateCaughtPokemon: vi.fn(),
1212
releasePokemon: vi.fn(),
13+
claimStarters: vi.fn(),
1314
},
1415
}));
1516

@@ -31,21 +32,16 @@ describe('Collection Page', () => {
3132

3233
it('should show loading state initially', () => {
3334
pokemonAPI.getCaughtPokemon.mockResolvedValue([]);
34-
3535
render(<Collection onNavigate={vi.fn()} />);
36-
37-
expect(screen.getByText(/Loading Pokémon/i)).toBeInTheDocument();
36+
expect(screen.getByText('⭐')).toBeInTheDocument();
3837
});
3938

4039
it('should show empty state when no pokemon caught', async () => {
4140
pokemonAPI.getCaughtPokemon.mockResolvedValue([]);
42-
4341
render(<Collection onNavigate={vi.fn()} />);
44-
4542
await waitFor(() => {
46-
expect(screen.queryByText(/Loading Pokémon/i)).not.toBeInTheDocument();
43+
expect(screen.queryByText('⭐')).not.toBeInTheDocument();
4744
});
48-
4945
expect(screen.getByText(/Your collection is empty/i)).toBeInTheDocument();
5046
expect(screen.getByText(/Explore Wild Pokémon/i)).toBeInTheDocument();
5147
});
@@ -55,9 +51,9 @@ describe('Collection Page', () => {
5551
{ id: 1, pokemon_id: 25, nickname: null, caught_date: '2024-01-01' },
5652
{ id: 2, pokemon_id: 1, nickname: 'Bulby', caught_date: '2024-01-02' },
5753
];
58-
54+
5955
pokemonAPI.getCaughtPokemon.mockResolvedValue(caughtPokemon);
60-
pokemonAPI.getPokemon.mockImplementation((id) =>
56+
pokemonAPI.getPokemon.mockImplementation((id) =>
6157
Promise.resolve({
6258
id,
6359
name: id === 25 ? 'Pikachu' : 'Bulbasaur',
@@ -67,42 +63,84 @@ describe('Collection Page', () => {
6763
image_url: 'https://example.com/image.png',
6864
})
6965
);
70-
66+
7167
render(<Collection onNavigate={vi.fn()} />);
72-
68+
7369
await waitFor(() => {
74-
expect(screen.queryByText(/Loading Pokémon/i)).not.toBeInTheDocument();
70+
expect(screen.queryByText('⭐')).not.toBeInTheDocument();
7571
});
76-
72+
7773
await waitFor(() => {
7874
expect(screen.getByText('Pikachu')).toBeInTheDocument();
7975
});
80-
76+
8177
expect(screen.getByText('Bulby')).toBeInTheDocument();
8278
});
8379

8480
it('should handle API errors gracefully', async () => {
8581
pokemonAPI.getCaughtPokemon.mockRejectedValue(new Error('Network error'));
86-
8782
render(<Collection onNavigate={vi.fn()} />);
88-
8983
await waitFor(() => {
90-
expect(screen.queryByText(/Loading Pokémon/i)).not.toBeInTheDocument();
84+
expect(screen.queryByText('⭐')).not.toBeInTheDocument();
9185
});
92-
9386
// Should show empty state or error message
9487
expect(screen.getByText(/Your collection is empty/i)).toBeInTheDocument();
9588
});
9689

9790
it('should have navigation buttons', async () => {
9891
pokemonAPI.getCaughtPokemon.mockResolvedValue([]);
92+
render(<Collection onNavigate={vi.fn()} />);
93+
await waitFor(() => {
94+
expect(screen.queryByText('⭐')).not.toBeInTheDocument();
95+
});
96+
expect(screen.getByText(/Explore Wild Pokémon/i)).toBeInTheDocument();
97+
});
98+
99+
it('should auto-claim starter pokemon for new users', async () => {
100+
const starterPokemon = [
101+
{ id: 1, pokemon_id: 'starter-1', nickname: null, caught_date: '2024-01-01' },
102+
{ id: 2, pokemon_id: 'starter-2', nickname: null, caught_date: '2024-01-01' },
103+
{ id: 3, pokemon_id: 'starter-3', nickname: null, caught_date: '2024-01-01' },
104+
];
99105

106+
// Empty at first, then has starters after claiming
107+
pokemonAPI.getCaughtPokemon
108+
.mockResolvedValueOnce([])
109+
.mockResolvedValueOnce(starterPokemon);
110+
111+
pokemonAPI.claimStarters.mockResolvedValue({
112+
success: true,
113+
message: 'Welcome to the world of Pokemon! You received 3 starter Pokemon!',
114+
starters: [
115+
{ caught_id: 1, name: 'Flametail Jr' },
116+
{ caught_id: 2, name: 'Ripplefin' },
117+
{ caught_id: 3, name: 'Leaflet' },
118+
],
119+
});
120+
100121
render(<Collection onNavigate={vi.fn()} />);
101122

102123
await waitFor(() => {
103-
expect(screen.queryByText(/Loading Pokémon/i)).not.toBeInTheDocument();
124+
expect(pokemonAPI.claimStarters).toHaveBeenCalledTimes(1);
104125
});
105126

106-
expect(screen.getByText(/Explore Wild Pokémon/i)).toBeInTheDocument();
127+
// Should fetch the collection twice (empty, then with starters)
128+
expect(pokemonAPI.getCaughtPokemon).toHaveBeenCalledTimes(2);
129+
});
130+
131+
it('should handle auto-claim failure gracefully', async () => {
132+
// Empty collection
133+
pokemonAPI.getCaughtPokemon.mockResolvedValue([]);
134+
pokemonAPI.claimStarters.mockRejectedValue(new Error('API Error'));
135+
136+
render(<Collection onNavigate={vi.fn()} />);
137+
138+
await waitFor(() => {
139+
expect(screen.queryByText('⭐')).not.toBeInTheDocument();
140+
});
141+
142+
// Should show empty state even if auto-claim failed
143+
expect(screen.getByText(/Your collection is empty/i)).toBeInTheDocument();
144+
expect(screen.getByText(/Claim Your Starter Pokémon/i)).toBeInTheDocument();
107145
});
108146
});

0 commit comments

Comments
 (0)