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
27 changes: 18 additions & 9 deletions country-explorer/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import './App.css';
import { useState } from 'react';
import { useCountries } from '../hooks/useCountries';
import { sortCountries, type SortOption } from '../utils/sortCountries';
import CountryCard from './CountryCard';
import CountrySelector from './CountrySelector';
import RegionFilter from './RegionFilter';
import SortSelector from './SortSelector';
import Loading from './Loading';
import ErrorMessage from './ErrorMessage';
import NavigationControls from './NavigationControls';
Expand All @@ -22,21 +24,24 @@ function App() {
const filteredCountries = selectedRegion
? countries.filter((country) => getRegion(country) === selectedRegion)
: countries;
const [sortOption, setSortOption] = useState<SortOption>('name-asc');
// A fresh, sorted copy — filteredCountries (and the cached query data it
// derives from) is left untouched.
const sortedCountries = sortCountries(filteredCountries, sortOption);
const [selectedCountryCode, setSelectedCountryCode] = useState<string | null>(null);
const selectedCountry =
filteredCountries.find((country) => country.code === selectedCountryCode) ??
filteredCountries[0];
sortedCountries.find((country) => country.code === selectedCountryCode) ?? sortedCountries[0];
const activeCountryCode = selectedCountry?.code ?? null;
const activeCountryIndex = filteredCountries.findIndex(
const activeCountryIndex = sortedCountries.findIndex(
(country) => country.code === activeCountryCode,
);

function navigateCountry(direction: -1 | 1) {
setSelectedCountryCode((currentCode) => {
const currentIndex = filteredCountries.findIndex((country) => country.code === currentCode);
if (currentIndex === -1) return filteredCountries[0]?.code ?? null;
const currentIndex = sortedCountries.findIndex((country) => country.code === currentCode);
if (currentIndex === -1) return sortedCountries[0]?.code ?? null;

return filteredCountries[currentIndex + direction]?.code ?? currentCode;
return sortedCountries[currentIndex + direction]?.code ?? currentCode;
});
}

Expand All @@ -56,15 +61,19 @@ function App() {
<main>
<section aria-labelledby="controls-heading">
<h2 id="controls-heading">Filters and selection</h2>
{/* SortSelector goes here */}
<RegionFilter
regions={availableRegions}
selectedRegion={selectedRegion}
onRegionChange={setSelectedRegion}
disabled={isLoading || isError}
/>
<SortSelector
sortOption={sortOption}
onSortChange={setSortOption}
disabled={isLoading || isError}
/>
<CountrySelector
countries={filteredCountries}
countries={sortedCountries}
selectedCountryCode={activeCountryCode}
onSelectCountry={setSelectedCountryCode}
disabled={isLoading || isError}
Expand All @@ -85,7 +94,7 @@ function App() {
<CountryCard country={selectedCountry} />
<NavigationControls
currentIndex={activeCountryIndex}
totalCountries={filteredCountries.length}
totalCountries={sortedCountries.length}
onPrevious={() => navigateCountry(-1)}
onNext={() => navigateCountry(1)}
/>
Expand Down
24 changes: 24 additions & 0 deletions country-explorer/src/components/SortSelector.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.sort-selector {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin: 1rem;
}

.sort-selector select {
min-width: 0;
max-width: 100%;
padding: 0.5rem;
font: inherit;
color: var(--text-h);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
}

.sort-selector select:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
33 changes: 33 additions & 0 deletions country-explorer/src/components/SortSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useId } from 'react';
import { SORT_OPTIONS, type SortOption } from '../utils/sortCountries';
import './SortSelector.css';

interface SortSelectorProps {
sortOption: SortOption;
onSortChange: (option: SortOption) => void;
disabled?: boolean;
}

function SortSelector({ sortOption, onSortChange, disabled = false }: SortSelectorProps) {
const selectId = useId();

return (
<div className="sort-selector">
<label htmlFor={selectId}>Sort by</label>
<select
id={selectId}
value={sortOption}
onChange={(event) => onSortChange(event.target.value as SortOption)}
disabled={disabled}
>
{SORT_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
);
}

export default SortSelector;
31 changes: 31 additions & 0 deletions country-explorer/src/utils/sortCountries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Country } from '../types/country';

export type SortOption = 'name-asc' | 'population-desc';

// Drives both the <select> in SortSelector and the switch below, so the
// two can never drift out of sync.
export const SORT_OPTIONS: { value: SortOption; label: string }[] = [
{ value: 'name-asc', label: 'Name (A–Z)' },
{ value: 'population-desc', label: 'Population (high to low)' },
];

/**
* Returns a new, sorted array. The input list (React Query's cached data)
* is never mutated — callers get a fresh copy to display.
*/
export function sortCountries(countries: Country[], sortOption: SortOption): Country[] {
const sorted = [...countries];

switch (sortOption) {
case 'population-desc':
// Countries with unknown population sort last either way.
sorted.sort((a, b) => (b.population ?? -Infinity) - (a.population ?? -Infinity));
break;
case 'name-asc':
default:
sorted.sort((a, b) => a.name.localeCompare(b.name));
break;
}

return sorted;
}