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
31 changes: 27 additions & 4 deletions country-explorer/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import './App.css';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useCountries } from '../hooks/useCountries';
import { sortCountries, type SortOption } from '../utils/sortCountries';
import { isSortOption, sortCountries, type SortOption } from '../utils/sortCountries';
import { readSessionValue, writeSessionValue } from '../utils/sessionStorage';
import CountryCard from './CountryCard';
import CountrySelector from './CountrySelector';
import RegionFilter from './RegionFilter';
Expand All @@ -10,9 +11,17 @@ import Loading from './Loading';
import ErrorMessage from './ErrorMessage';
import NavigationControls from './NavigationControls';

// sessionStorage keys for the display choices this component persists.
const REGION_STORAGE_KEY = 'selectedRegion';
const SORT_STORAGE_KEY = 'sortOption';

function App() {
const { countries, isLoading, isError, error, refetch } = useCountries();
const [selectedRegion, setSelectedRegion] = useState('');
// Lazily read any saved choice on first render, so the page opens already
// showing what the user had before reloading within the same session.
const [selectedRegion, setSelectedRegion] = useState(
() => readSessionValue(REGION_STORAGE_KEY) ?? '',
);
// The fetched countries are European, so filter by their geographic subregion.
const getRegion = (country: (typeof countries)[number]) =>
country.subregion?.trim() || country.region?.trim() || 'Unknown region';
Expand All @@ -24,7 +33,11 @@ function App() {
const filteredCountries = selectedRegion
? countries.filter((country) => getRegion(country) === selectedRegion)
: countries;
const [sortOption, setSortOption] = useState<SortOption>('name-asc');
const [sortOption, setSortOption] = useState<SortOption>(() => {
const stored = readSessionValue(SORT_STORAGE_KEY);
// A missing or no-longer-valid stored value falls back to the default.
return isSortOption(stored) ? stored : 'name-asc';
});
// A fresh, sorted copy — filteredCountries (and the cached query data it
// derives from) is left untouched.
const sortedCountries = sortCountries(filteredCountries, sortOption);
Expand All @@ -36,6 +49,16 @@ function App() {
(country) => country.code === activeCountryCode,
);

// Persist the display choices (never the fetched country/API data) so a
// reload within the same browser session restores them.
useEffect(() => {
writeSessionValue(REGION_STORAGE_KEY, selectedRegion);
}, [selectedRegion]);

useEffect(() => {
writeSessionValue(SORT_STORAGE_KEY, sortOption);
}, [sortOption]);

function navigateCountry(direction: -1 | 1) {
setSelectedCountryCode((currentCode) => {
const currentIndex = sortedCountries.findIndex((country) => country.code === currentCode);
Expand Down
29 changes: 29 additions & 0 deletions country-explorer/src/utils/sessionStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Thin, defensive wrapper around sessionStorage for small UI display
* choices (region filter, sort option) — never the fetched country data
* itself. Keys are namespaced so they don't collide with anything else on
* the page, and every call is wrapped in a try/catch since storage can be
* unavailable or full (e.g. private browsing, quota exceeded) — a failed
* read/write should never crash the app.
*/
const NAMESPACE = 'country-explorer';

function storageKey(key: string): string {
return `${NAMESPACE}:${key}`;
}

export function readSessionValue(key: string): string | null {
try {
return window.sessionStorage.getItem(storageKey(key));
} catch {
return null;
}
}

export function writeSessionValue(key: string, value: string): void {
try {
window.sessionStorage.setItem(storageKey(key), value);
} catch {
// Storage unavailable or full — the choice just won't persist.
}
}
6 changes: 6 additions & 0 deletions country-explorer/src/utils/sortCountries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export const SORT_OPTIONS: { value: SortOption; label: string }[] = [
{ value: 'population-desc', label: 'Population (high to low)' },
];

// Used to validate a sort option loaded from sessionStorage — a stored
// value that isn't one of these is treated as missing/invalid.
export function isSortOption(value: string | null): value is SortOption {
return SORT_OPTIONS.some((option) => option.value === value);
}

/**
* Returns a new, sorted array. The input list (React Query's cached data)
* is never mutated — callers get a fresh copy to display.
Expand Down