diff --git a/t29-project-1/server/catApi.js b/t29-project-1/server/catApi.js index acb17d9..6ca1d4b 100644 --- a/t29-project-1/server/catApi.js +++ b/t29-project-1/server/catApi.js @@ -49,8 +49,23 @@ export async function fetchCatFromApi(breedIds) { return data[0] || {}; } -export async function fetchBreedsFromApi() { +const DEFAULT_BREEDS_PAGE = 0; +const DEFAULT_BREEDS_LIMIT = 10; + +function parseNonNegativeInt(value, fallback) { + const parsed = Number(value); + return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback; +} + +export async function fetchBreedsFromApi({ page, limit } = {}) { const breedsUrl = new URL(CAT_BREEDS_URL); - breedsUrl.searchParams.set('limit', '10'); + breedsUrl.searchParams.set( + 'limit', + String(parseNonNegativeInt(limit, DEFAULT_BREEDS_LIMIT)) + ); + breedsUrl.searchParams.set( + 'page', + String(parseNonNegativeInt(page, DEFAULT_BREEDS_PAGE)) + ); return fetchApiResponse(breedsUrl); } diff --git a/t29-project-1/server/catApi.test.js b/t29-project-1/server/catApi.test.js index 7a6cde7..0413344 100644 --- a/t29-project-1/server/catApi.test.js +++ b/t29-project-1/server/catApi.test.js @@ -70,7 +70,7 @@ describe('fetchCatFromApi', () => { }); describe('fetchBreedsFromApi', () => { - it('requests up to ten breeds', async () => { + it('defaults to the first page of ten breeds', async () => { const breeds = [{ id: 'abys', name: 'Abyssinian' }]; fetchMock.mockResolvedValue(mockResponse(breeds)); @@ -79,5 +79,26 @@ describe('fetchBreedsFromApi', () => { const requestUrl = new URL(String(fetchMock.mock.calls[0][0])); expect(requestUrl.pathname).toBe('/v1/breeds'); expect(requestUrl.searchParams.get('limit')).toBe('10'); + expect(requestUrl.searchParams.get('page')).toBe('0'); + }); + + it('requests the given page and limit', async () => { + fetchMock.mockResolvedValue(mockResponse([])); + + await fetchBreedsFromApi({ page: 2, limit: 5 }); + + const requestUrl = new URL(String(fetchMock.mock.calls[0][0])); + expect(requestUrl.searchParams.get('limit')).toBe('5'); + expect(requestUrl.searchParams.get('page')).toBe('2'); + }); + + it('falls back to the defaults for invalid page or limit', async () => { + fetchMock.mockResolvedValue(mockResponse([])); + + await fetchBreedsFromApi({ page: 'not-a-number', limit: -1 }); + + const requestUrl = new URL(String(fetchMock.mock.calls[0][0])); + expect(requestUrl.searchParams.get('limit')).toBe('10'); + expect(requestUrl.searchParams.get('page')).toBe('0'); }); }); diff --git a/t29-project-1/server/index.js b/t29-project-1/server/index.js index 7064f98..382cb89 100644 --- a/t29-project-1/server/index.js +++ b/t29-project-1/server/index.js @@ -10,7 +10,12 @@ app.use(cors()); app.get('/api/breeds', async (req, res) => { try { - res.json(await fetchBreedsFromApi()); + res.json( + await fetchBreedsFromApi({ + page: req.query.page, + limit: req.query.limit, + }) + ); } catch (err) { console.error(err); res.status(err.statusCode || 500).send(err.message); diff --git a/t29-project-1/src/App.tsx b/t29-project-1/src/App.tsx index 0fe05bb..3d4b451 100644 --- a/t29-project-1/src/App.tsx +++ b/t29-project-1/src/App.tsx @@ -1,11 +1,15 @@ +import { useState } from 'react'; import { useCatNavigation } from './hooks/useCatNavigation'; import { useFavoriteCat } from './hooks/useFavoriteCat'; import { NavPrevious, NavNext } from './components/ActionButtons'; +import { BreedDropdown } from './components/BreedDropdown'; import { CatCard } from './components/CatCard'; import { Link } from 'react-router-dom'; import './App.css'; export default function App() { + const [selectedBreedId, setSelectedBreedId] = useState(null); + const { currentCat, isLoading, @@ -13,11 +17,18 @@ export default function App() { canGoPrevious, handleNext, handlePrevious, - } = useCatNavigation(); + } = useCatNavigation(selectedBreedId); const { isFavorite, toggleFavorite } = useFavoriteCat(currentCat); + return (
Favorite + + +