Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions country-explorer/src/api/countries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] : [])),
};
}

Expand Down
21 changes: 20 additions & 1 deletion country-explorer/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<header>
Expand All @@ -16,7 +23,19 @@ function App() {

<section aria-labelledby="country-heading">
<h2 id="country-heading">Selected country</h2>
{/* CountryCard, FavoriteButton, and NavigationControls go here */}
{isLoading ? (
<Loading message="Loading countries…" />
) : isError ? (
<ErrorMessage
message={error?.message ?? 'Unable to load countries.'}
onRetry={() => void refetch()}
/>
) : selectedCountry ? (
<CountryCard country={selectedCountry} />
) : (
<p>No countries available.</p>
)}
{/* FavoriteButton and NavigationControls go here */}
</section>

<section aria-labelledby="favorites-heading">
Expand Down
52 changes: 52 additions & 0 deletions country-explorer/src/components/CountryCard.css
Original file line number Diff line number Diff line change
@@ -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;
}
62 changes: 62 additions & 0 deletions country-explorer/src/components/CountryCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<article className="country-card" aria-labelledby={headingId}>
{country.flagUrl ? (
<img className="country-card__flag" src={country.flagUrl} alt={`Flag of ${name}`} />
) : (
<p className="country-card__flag-placeholder">Flag not available</p>
)}
<div className="country-card__content">
<h3 id={headingId}>{name}</h3>
<dl className="country-card__details">
<div>
<dt>Capital</dt>
<dd>{country.capital?.trim() || unavailable}</dd>
</div>
<div>
<dt>Region</dt>
<dd>{country.region?.trim() || unavailable}</dd>
</div>
<div>
<dt>Population</dt>
<dd>{formatNumber(country.population)}</dd>
</div>
<div>
<dt>Area</dt>
<dd>{formatNumber(country.area, ' km²')}</dd>
</div>
<div>
<dt>Languages</dt>
<dd>{country.languages?.filter(Boolean).join(', ') || unavailable}</dd>
</div>
<div>
<dt>Currency</dt>
<dd>{country.currencies?.filter(Boolean).join(', ') || unavailable}</dd>
</div>
</dl>
</div>
</article>
);
}

export default CountryCard;
40 changes: 22 additions & 18 deletions country-explorer/src/types/country.ts
Original file line number Diff line number Diff line change
@@ -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}.
Expand All @@ -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[];
Expand Down