diff --git a/country-explorer/package-lock.json b/country-explorer/package-lock.json index c394623..0ef26fc 100644 --- a/country-explorer/package-lock.json +++ b/country-explorer/package-lock.json @@ -60,6 +60,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -886,6 +887,7 @@ "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -896,6 +898,7 @@ "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -955,6 +958,7 @@ "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.70.0", "@typescript-eslint/types": "8.70.0", @@ -1189,6 +1193,7 @@ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1279,6 +1284,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", @@ -1428,6 +1434,7 @@ "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", "dev": true, "license": "MIT", + "peer": true, "workspaces": [ "packages/*" ], @@ -1899,6 +1906,7 @@ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -2346,6 +2354,7 @@ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2443,6 +2452,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2591,6 +2601,7 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2677,6 +2688,7 @@ "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", @@ -2801,6 +2813,7 @@ "integrity": "sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/country-explorer/src/api/countries.ts b/country-explorer/src/api/countries.ts index ab2c116..dce6375 100644 --- a/country-explorer/src/api/countries.ts +++ b/country-explorer/src/api/countries.ts @@ -1,9 +1,7 @@ -import type { Country } from '../types/country'; - const BASE_URL = 'https://countries.dev'; -// Thrown for any non-2xx response. `status` lets callers distinguish a -// "no result" 404 (show an empty state) from a real failure. +// Thrown for a failed request: a non-2xx response (status holds the real +// HTTP status) or a network-level failure (status 0). export class ApiError extends Error { status: number; @@ -14,16 +12,69 @@ export class ApiError extends Error { } } -async function handleResponse(response: Response): Promise { +// The internal shape this module returns to the rest of the app. This is a +// placeholder until #8 ("Define TypeScript Types for Country Data") lands +// its own Country type in src/types/country.ts — once it does, delete this +// and the RawCountry type below, import that one instead, and point +// `toCountry` at it. Kept local to this file (instead of touching +// src/types/country.ts) so the two issues can be worked on in parallel +// without both branches editing the same file. +export interface Country { + name: string; + capital: string; + region: string; + subregion: string; + population: number; + area: number; + flagUrl: string; + currencies: string[]; + languages: string[]; +} + +// The relevant slice of the raw shape returned by https://countries.dev. +interface RawCountry { + name: string; + capital: string; + region: string; + subregion: string; + population: number; + area: number; + flags: { svg: string; png: string }; + currencies: { code: string; name: string; symbol: string }[]; + languages: { name: string }[]; +} + +function toCountry(raw: RawCountry): Country { + return { + name: raw.name, + capital: raw.capital, + region: raw.region, + subregion: raw.subregion, + population: raw.population, + area: raw.area, + flagUrl: raw.flags.svg, + currencies: raw.currencies.map((currency) => `${currency.name} (${currency.code})`), + languages: raw.languages.map((language) => language.name), + }; +} + +async function fetchJson(url: string): Promise { + let response: Response; + try { + response = await fetch(url); + } catch { + throw new ApiError(0, 'Network request failed. Check your internet connection.'); + } + if (!response.ok) { - let message = response.statusText; + let message = response.statusText || `Request failed with status ${response.status}`; try { const body: unknown = await response.json(); if (body && typeof body === 'object' && 'error' in body) { message = String((body as { error: unknown }).error); } } catch { - // Response body wasn't JSON; fall back to statusText. + // Response body wasn't JSON; fall back to the status text above. } throw new ApiError(response.status, message); } @@ -31,14 +82,14 @@ async function handleResponse(response: Response): Promise { return response.json() as Promise; } -// GET /region/{region} — all countries in a region, e.g. "europe". +// GET /region/{region} — every country in a region, e.g. "europe". export async function getCountriesByRegion(region: string): Promise { - const response = await fetch(`${BASE_URL}/region/${encodeURIComponent(region)}`); - return handleResponse(response); + const raw = await fetchJson(`${BASE_URL}/region/${encodeURIComponent(region)}`); + return raw.map(toCountry); } // GET /alpha/{code} — a single country by ISO alpha-2 or alpha-3 code. export async function getCountryByAlpha(code: string): Promise { - const response = await fetch(`${BASE_URL}/alpha/${encodeURIComponent(code)}`); - return handleResponse(response); + const raw = await fetchJson(`${BASE_URL}/alpha/${encodeURIComponent(code)}`); + return toCountry(raw); }