From 80c7eddf4b299c4b3b90f45d398fe88c9eb6dc5e Mon Sep 17 00:00:00 2001 From: Edvin Gunic Date: Mon, 14 Sep 2026 01:40:12 +0200 Subject: [PATCH] feat: add reusable country card Display country details through props, format numbers, add flag alt text and responsive styling, handle missing API fields, and show one country at a time. Closes #13 --- country-explorer/src/api/countries.ts | 22 ++++--- country-explorer/src/components/App.tsx | 21 ++++++- .../src/components/CountryCard.css | 52 ++++++++++++++++ .../src/components/CountryCard.tsx | 62 +++++++++++++++++++ country-explorer/src/types/country.ts | 40 ++++++------ 5 files changed, 169 insertions(+), 28 deletions(-) create mode 100644 country-explorer/src/components/CountryCard.css create mode 100644 country-explorer/src/components/CountryCard.tsx diff --git a/country-explorer/src/api/countries.ts b/country-explorer/src/api/countries.ts index 4f9144f..31de31e 100644 --- a/country-explorer/src/api/countries.ts +++ b/country-explorer/src/api/countries.ts @@ -17,15 +17,19 @@ export class ApiError extends Error { function toCountry(raw: CountryApiResponse): Country { return { code: raw.alpha3Code, - 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), + name: raw.name ?? '', + capital: raw.capital ?? '', + region: raw.region ?? '', + subregion: raw.subregion ?? '', + population: raw.population ?? null, + area: raw.area ?? null, + flagUrl: raw.flags?.svg ?? '', + currencies: (raw.currencies ?? []) + .map((currency) => + [currency.name, currency.code ? `(${currency.code})` : ''].filter(Boolean).join(' '), + ) + .filter(Boolean), + languages: (raw.languages ?? []).flatMap((language) => (language.name ? [language.name] : [])), }; } diff --git a/country-explorer/src/components/App.tsx b/country-explorer/src/components/App.tsx index deb916d..ce8be6c 100644 --- a/country-explorer/src/components/App.tsx +++ b/country-explorer/src/components/App.tsx @@ -1,6 +1,13 @@ import './App.css'; +import { useCountries } from '../hooks/useCountries'; +import CountryCard from './CountryCard'; +import Loading from './Loading'; +import ErrorMessage from './ErrorMessage'; function App() { + const { countries, isLoading, isError, error, refetch } = useCountries(); + const selectedCountry = countries[0]; + return ( <>
@@ -16,7 +23,19 @@ function App() {

Selected country

- {/* CountryCard, FavoriteButton, and NavigationControls go here */} + {isLoading ? ( + + ) : isError ? ( + void refetch()} + /> + ) : selectedCountry ? ( + + ) : ( +

No countries available.

+ )} + {/* FavoriteButton and NavigationControls go here */}
diff --git a/country-explorer/src/components/CountryCard.css b/country-explorer/src/components/CountryCard.css new file mode 100644 index 0000000..de7df82 --- /dev/null +++ b/country-explorer/src/components/CountryCard.css @@ -0,0 +1,52 @@ +.country-card { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + margin: 1rem; + padding: clamp(1rem, 3vw, 2rem); + border: 1px solid var(--border); + border-radius: 12px; + background: var(--bg); + text-align: left; +} + +.country-card__flag, +.country-card__flag-placeholder { + width: 100%; + max-width: 16rem; + align-self: flex-start; +} + +.country-card__flag { + height: auto; + object-fit: contain; +} + +.country-card__content { + flex: 1 1 16rem; + min-width: 0; + overflow-wrap: anywhere; +} + +.country-card h3 { + margin: 0 0 1rem; + color: var(--text-h); + font-size: 1.5rem; + line-height: 1.3; +} + +.country-card__details { + display: grid; + gap: 1rem; + margin: 0; +} + +.country-card__details dt { + color: var(--text-h); + font-weight: 600; +} + +.country-card__details dd { + margin: 0.25rem 0 0; + font-variant-numeric: tabular-nums; +} diff --git a/country-explorer/src/components/CountryCard.tsx b/country-explorer/src/components/CountryCard.tsx new file mode 100644 index 0000000..8ff5923 --- /dev/null +++ b/country-explorer/src/components/CountryCard.tsx @@ -0,0 +1,62 @@ +import { useId } from 'react'; +import type { Country } from '../types/country'; +import './CountryCard.css'; + +interface CountryCardProps { + country: Country; +} + +const numberFormatter = new Intl.NumberFormat('en-GB'); +const unavailable = 'Not available'; + +function formatNumber(value: number | null | undefined, unit = '') { + return typeof value === 'number' && Number.isFinite(value) && value >= 0 + ? `${numberFormatter.format(value)}${unit}` + : unavailable; +} + +function CountryCard({ country }: CountryCardProps) { + const headingId = useId(); + const name = country.name?.trim() || 'Unknown country'; + + return ( +
+ {country.flagUrl ? ( + {`Flag + ) : ( +

Flag not available

+ )} +
+

{name}

+
+
+
Capital
+
{country.capital?.trim() || unavailable}
+
+
+
Region
+
{country.region?.trim() || unavailable}
+
+
+
Population
+
{formatNumber(country.population)}
+
+
+
Area
+
{formatNumber(country.area, ' km²')}
+
+
+
Languages
+
{country.languages?.filter(Boolean).join(', ') || unavailable}
+
+
+
Currency
+
{country.currencies?.filter(Boolean).join(', ') || unavailable}
+
+
+
+
+ ); +} + +export default CountryCard; diff --git a/country-explorer/src/types/country.ts b/country-explorer/src/types/country.ts index 3e9a2b4..5b027f1 100644 --- a/country-explorer/src/types/country.ts +++ b/country-explorer/src/types/country.ts @@ -1,22 +1,26 @@ // Relevant fields from a successful single-country API response. export interface CountryApiResponse { alpha3Code: string; - name: string; - capital: string; - region: string; - subregion: string; - population: number; - area: number; - flags: { - svg: string; - }; - currencies: { - code: string; - name: string; - }[]; - languages: { - name: string; - }[]; + name?: string | null; + capital?: string | null; + region?: string | null; + subregion?: string | null; + population?: number | null; + area?: number | null; + flags?: { + svg?: string | null; + } | null; + currencies?: + | { + code?: string | null; + name?: string | null; + }[] + | null; + languages?: + | { + name?: string | null; + }[] + | null; } // Successful response from /region/{region}. @@ -29,8 +33,8 @@ export interface Country { capital: string; region: string; subregion: string; - population: number; - area: number; + population: number | null; + area: number | null; flagUrl: string; currencies: string[]; languages: string[];