diff --git a/README.md b/README.md index f161842..3f1ab05 100644 --- a/README.md +++ b/README.md @@ -1,803 +1,369 @@ # Utforsk verden -[Åpne appen på gruppas VM](http://it2810-36.idi.ntnu.no/project1/) +**Publisert app:** [http://it2810-36.idi.ntnu.no/project1/](http://it2810-36.idi.ntnu.no/project1/) (krever NTNUs nett eller VPN) + +Gruppeprosjekt 1 i IT2810 Webutvikling, høsten 2026, gruppe T36. + +Utforsk verden henter data om de 25 mest folkerike landene i verden fra et åpent REST-API og viser ett land om gangen. Brukeren kan bla mellom landene, hoppe direkte til et land, sortere listen, markere favoritter og vise bare favorittene. + +Mer dokumentasjon: + +- [Testing](docs/testing.md): automatiske tester og alle gjennomførte tester i nettlesere og på enheter +- [Tilgjengelighet](docs/accessibility.md): sjekkliste for universell utforming +- [Publisering](docs/publisering.md): slik legges appen ut på gruppas VM +- [Prosjektstandard](docs/prosjektstandard.md): issues, branches, commits, pull requests og Definition of Done + +## Innhold + +1. [Funksjoner](#funksjoner) +2. [Kom i gang](#kom-i-gang) +3. [Teknologi og struktur](#teknologi-og-struktur) +4. [Datakilde og datautvalg](#datakilde-og-datautvalg) +5. [Henting med TanStack Query](#henting-med-tanstack-query) +6. [State og props i React](#state-og-props-i-react) +7. [Lagring i nettleseren](#lagring-i-nettleseren) +8. [Responsivt design](#responsivt-design) +9. [Tilgjengelighet](#tilgjengelighet) +10. [Testing](#testing) +11. [Kjente begrensninger](#kjente-begrensninger) +12. [Arbeidsflyt](#arbeidsflyt) +13. [Bruk av KI](#bruk-av-ki) + +## Funksjoner + +| Krav i oppgaven | Slik er det løst | +| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| Én ressurs om gangen, bla frem og tilbake og hoppe direkte | Landkort med «Forrige» og «Neste», og en klikkbar landliste i sidemenyen | +| Valg som påvirker utvalget og huskes ved reload | Sortering etter navn eller befolkning og filteret «Vis bare favoritter», lagret i sessionStorage | +| Favoritter som huskes etter at nettleseren lukkes | Knappen «Favoritt» med stjerne på landkortet, lagret i localStorage | +| Responsivt design | Sidemeny og landkort ved siden av hverandre på store skjermer, én kolonne på mobil og eget oppsett for mobil i liggende format | +| REST-API med TanStack Query | Ett kall til countries.dev per sidelast, se [Henting med TanStack Query](#henting-med-tanstack-query) | +| Tilgjengelig HTML | Semantiske elementer, tastaturstøtte, synlig fokus og god kontrast, se [Tilgjengelighet](#tilgjengelighet) | + +- **Landkort:** Navn, flagg, hovedstad, region, befolkning, areal og språk. Tall vises med norsk format (`1 402 112 000` og `km²`), og flere språk skrives som «Hindi og English». Manglende verdier vises som «Ikke oppgitt», og manglende flagg som «Flagg ikke tilgjengelig». +- **Blaing:** «Forrige», «Neste» og posisjonen «Land 3 av 25» over kortet. Knappene er deaktivert ved første og siste land. +- **Landliste:** Sidemenyen viser alle land i utvalget. Valgt land er markert, og favoritter har en stjerne. På mobil ligger listen bak knappen «Landliste (25)». +- **Sortering:** «Navn (A–Å)» etter norsk alfabet, eller «Befolkning (høyest først)». Landet som vises, beholdes når sorteringen endres. +- **Favoritter og filter:** Stjerneknappen legger landet til eller fjerner det som favoritt. «Vis bare favoritter» begrenser landlisten, blaingen og posisjonen til favorittene. Er filteret på uten favoritter, vises en melding og knappen «Vis alle land». +- **Lasting og feil:** «Laster land …» mens dataene hentes. Ved feil vises en norsk feilmelding og knappen «Prøv igjen», og ved tomt svar vises «Fant ingen land.». +- **Kildehenvisning:** Datakilden og flaggkilden er oppgitt nederst på siden. + +## Kom i gang + +### Krav + +- Node.js 24.6.0 eller nyere. `.nvmrc` peker på Node 24, så med nvm holder det å kjøre `nvm install` og `nvm use`. +- npm 11 eller nyere. + +Kravene står også i `engines` i `package.json`. -Gruppeprosjekt i IT2810 Webutvikling, høsten 2026. -Gruppe: T36. +### Installasjon -Applikasjonen skal la brukeren utforske ett land om gangen, -bla mellom land, sortere visningen og lagre favoritter. +```bash +git clone git@git.ntnu.no:IT2810-H26/T36-Project-1.git +cd T36-Project-1 +npm ci +``` -## Status +Uten SSH-nøkkel kan repoet klones med `https://git.ntnu.no/IT2810-H26/T36-Project-1.git`. `npm ci` installerer nøyaktig de versjonene som står i `package-lock.json`. Appen trenger ingen API-nøkkel eller `.env`-fil. -Prosjektoppsettet er på plass med React, TypeScript, Vite, -ESLint og Prettier. Appen henter landdata fra et REST-API med -TanStack Query og viser ett land om gangen i et landkort. -Brukeren kan bla med forrige/neste, velge land direkte i en sidemeny og -sortere landene etter navn eller befolkning. Sorteringsvalget huskes etter -reload i samme fane. Brukeren kan markere favorittland, og favorittene -huskes mellom nettleserøkter. Et filter kan vise bare favorittland, og -filtervalget huskes etter reload i samme fane. Layouten tilpasser seg -desktop, nettbrett og mobil i både stående og liggende format. +### Kommandoer -## Krav +| Kommando | Hva den gjør | +| ---------------------- | ------------------------------------------------------------- | +| `npm run dev` | Starter utviklingsserveren på http://localhost:5173/project1/ | +| `npm run build` | Typesjekker med `tsc -b` og bygger appen til `dist/` | +| `npm run preview` | Viser produksjonsbygget på http://localhost:4173/project1/ | +| `npm run lint` | Kontrollerer koden med ESLint | +| `npm run format:check` | Kontrollerer formateringen med Prettier | +| `npm run format` | Formaterer alle filer med Prettier | +| `npm run test` | Kjører alle testene én gang med Vitest | +| `npm run test:watch` | Kjører testene på nytt ved hver endring | -- Node.js 24.6.0 eller nyere. Gruppen bruker Node 24. -- npm 11 eller nyere. +Adressene slutter på `/project1/` fordi appen er bygget for å ligge i den mappen på VM-en. Stopp en server med Ctrl+C. -Hvis du bruker nvm: +Kjør alle kontrollene med én kommando, for eksempel før en pull request: ```bash -nvm install -nvm use +npm run lint && npm run format:check && npm run test && npm run build ``` -## Installasjon +Slik legges appen ut på VM-en: se [publiseringsveiledningen](docs/publisering.md). -Klon repoet og gå inn i prosjektmappen: +## Teknologi og struktur -```bash -git clone git@git.ntnu.no:IT2810-H26/T36-Project-1.git -cd T36-Project-1 +- React 19 og TypeScript, satt opp med `npm create vite@latest` og malen for React og TypeScript +- TanStack Query 5 til henting fra REST-API-et +- Vanlig CSS, én fil per komponent. Ingen UI-biblioteker eller ferdige komponenter. +- ESLint og Prettier +- Vitest og Testing Library + +```text +src/ +├── api/countries.ts fetchCountries: henter, kontrollerer og gjør om API-svaret +├── hooks/ useCountries (TanStack Query) og hooks for lagrede valg +├── components/ CountryCard, CountryMenu, CountryNavigation, SortControl, +│ FavoritesFilter og FavoriteButton, hver med CSS og test +├── utils/ sortering og lesing og lagring i Web Storage +├── types/country.ts typen Country +├── test/ felles testoppsett og hjelpefunksjoner +├── App.tsx eier valgt land og utvalget og setter sammen komponentene +└── main.tsx oppretter QueryClient og starter React +docs/ testing, tilgjengelighet, publisering og prosjektstandard ``` -Installer avhengigheter fra låsefilen: +Dataene flyter én vei, fra API-et gjennom `App` og ned til komponentene: -```bash -npm ci +```text +countries.dev ─▶ fetchCountries ─▶ useCountries (cache i TanStack Query) + │ data + ▼ + App: sorter ▶ filtrer ▶ finn valgt land + │ props │ props │ props + ▼ ▼ ▼ + CountryMenu CountryNavigation CountryCard + (SortControl, FavoritesFilter) (FavoriteButton) ``` -## Lokal utvikling +## Datakilde og datautvalg -```bash -npm run dev -``` +### Kilder -Åpne adressen som vises i terminalen, typisk -http://localhost:5173/project1/. -Stopp utviklingsserveren med Ctrl + C. +- **Landdata:** [countries.dev](https://countries.dev) ([dokumentasjon](https://countries.dev/docs)). API-et er gratis, krever ingen nøkkel og tillater kall direkte fra nettleseren (CORS). Dataene bygger på [GeoNames](https://www.geonames.org) og er lisensiert under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.no), som krever at kilden oppgis. +- **Flagg:** Bildeadressene i API-svaret peker til [flagcdn.com](https://flagcdn.com), en gratis tjeneste fra [Flagpedia.net](https://flagpedia.net), som ber om en lenke tilbake. +- Begge kildene er oppgitt nederst i appen ([`App.tsx`](src/App.tsx#L152)). -## Kodekontroll og formatering +REST Countries var førstevalget, men nyere versjoner av det API-et krever registrering. -Kontroller koden med ESLint: +### Datautvalg -```bash -npm run lint +Appen gjør én forespørsel ([`src/api/countries.ts`](src/api/countries.ts#L9)): + +```text +https://countries.dev/countries?fields=alpha3Code,name,flags,capital,region,population,area,languages&sort=population&order=desc&limit=25 ``` -Kontroller formatering: +- **Antall land:** De 25 mest folkerike landene. Antallet settes med `COUNTRY_LIMIT`. Ingen land fra Oseania kommer med i utvalget. +- **Felter:** Bare feltene appen bruker: landkode, navn, flagg, hovedstad, region, befolkning, areal og språk. +- **Identitet:** Landkoden `alpha3Code` (ISO 3166-1 alpha-3, for eksempel `NOR`) brukes som identitet og React-key. +- **Kontroll av svaret:** Svaret gjøres om til typen `Country` i `src/types/country.ts`. Er svaret ikke en array, blir det en feil. Land uten kode eller navn hoppes over. Andre felter kan mangle i API-et og er derfor valgfrie i typen, bortsett fra `languages`, som blir en tom liste. +- **Ingen lagring i nettleseren:** Dataene hentes fra API-et hver gang appen lastes. TanStack Query holder dem i minnet mens appen er åpen, men landdata lagres aldri i localStorage, sessionStorage eller IndexedDB. Bare brukerens valg lagres. -```bash -npm run format:check -``` +## Henting med TanStack Query -Formater filene automatisk: +Hentingen skjer i [`useCountries`](src/hooks/useCountries.ts), som kaller `useQuery`. -```bash -npm run format -``` +- **Én QueryClient:** Den opprettes én gang utenfor React i [`main.tsx`](src/main.tsx#L8), slik at cachen ikke nullstilles når komponenter rendres på nytt. +- **Fast query key:** [`['countries']`](src/hooks/useCountries.ts#L7). Alle komponenter som bruker hooken, deler samme data og samme forespørsel. +- **Avbrudd:** TanStack Query sender med et `AbortSignal`, som [`fetchCountries`](src/api/countries.ts#L17) sender videre til `fetch`. +- **`staleTime` er én time** ([linje 13](src/hooks/useCountries.ts#L13)). Landdata endres sjelden, og API-et oppgir selv `Cache-Control: max-age=3600`. Så lenge dataene er ferske, gir nye rendringer og nye komponenter ingen nye kall. +- **`refetchOnWindowFocus: false`** ([linje 15](src/hooks/useCountries.ts#L15)), så bytte av fane eller vindu gir ingen nye kall. +- **`refetchOnReconnect` og `refetchOnMount`** har standardverdien `true`, men gjelder bare data som er eldre enn `staleTime`. Kommer nettet tilbake når dataene er mer enn én time gamle, hentes de på nytt. Ellers skjer ingenting. `App` monteres bare én gang. +- **`retry: 1`** ([linje 20](src/hooks/useCountries.ts#L20)) gir ett automatisk nytt forsøk, også etter «Prøv igjen». Deretter vises feilmeldingen. Flere forsøk ville bare gjort feilmeldingen tregere. +- **`gcTime`** har standardverdien fem minutter. Den har ingen betydning her, fordi `App` alltid bruker queryen. +- **Bare i minnet:** Cachen forsvinner ved reload, og da hentes dataene på nytt. +- **Lokale endringer:** Sortering, favoritter, filter, blaing og valg av land skjer på dataene som allerede er hentet, og gir ingen nye kall. -## Produksjonsbygg +**Resultat:** Produksjonsbygget gjør ett kall per sidelast. Det er målt i Chrome, Firefox og WebKit, se [testing](docs/testing.md#skriptet-nettlesertest-av-produksjonsbygget-17). -```bash -npm run build -``` +**Utviklingsmodus:** React Strict Mode er beholdt. I `npm run dev` monterer Strict Mode komponentene to ganger. Den første forespørselen avbrytes da, og en ny startes. Nettverksfanen viser derfor én avbrutt og én fullført forespørsel. Dette skjer bare i utvikling. -Kommandoen kontrollerer TypeScript og lager produksjonsbygget -i `dist`. +### Feilhåndtering -Forhåndsvis bygget lokalt: +`fetchCountries` gjør feil om til norske meldinger. `App` viser «Kunne ikke hente landdata.» fulgt av meldingen, og knappen «Prøv igjen», som kaller `refetch`. -```bash -npm run preview -``` +| Situasjon | Melding | +| ------------------------------------------ | ------------------------------------------------------- | +| Ingen kontakt (nettverks- eller CORS-feil) | «Fikk ikke kontakt med API-et. Sjekk nettforbindelsen.» | +| HTTP-feil, for eksempel 429 eller 500 | «API-et svarte med en feil (HTTP 500).» | +| Svaret er ikke gyldig JSON | «Svaret fra API-et var ikke gyldig JSON.» | +| Svaret er ikke en array | «Svaret fra API-et hadde et uventet format.» | -Bygget ligger under `/project1/`, så forhåndsvisningen åpnes på -http://localhost:4173/project1/. +- Den opprinnelige feilen legges ved som `cause`, så den kan undersøkes ved feilsøking. +- Feilen fra en avbrutt forespørsel kastes videre uendret. TanStack Query håndterer den selv, og den vises ikke som feil. +- Tomt svar (`[]`) gir «Fant ingen land.». +- **Feil etter vellykket henting:** Feiler en senere henting i bakgrunnen, beholder TanStack Query de gamle dataene. `App` viser feilsiden bare når det ikke finnes data ([`App.tsx`](src/App.tsx#L69)), så brukeren kan fortsette med landene som allerede er hentet. -## Publisering på gruppas VM +## State og props i React -Appen kjører på -[http://it2810-36.idi.ntnu.no/project1/](http://it2810-36.idi.ntnu.no/project1/). -VM-en er tilgjengelig på NTNUs nett eller VPN. +`App` eier tilstanden og sender data og funksjoner ned til komponentene som props. Komponentene viser det de får, og melder endringer tilbake gjennom funksjoner som `onSelect`, `onChange` og `onToggle`. `SortControl` og `FavoritesFilter` er kontrollerte skjemaelementer: verdien kommer fra props, ikke fra egen state. Bare tilstand som gjelder visningen i én komponent, som om landlisten er åpen på mobil, ligger lokalt i komponenten. Landdataene er servertilstand og eies av TanStack Query, ikke av `useState`. -For å legge ut en ny versjon: +### State -1. Kjør `npm ci` og `npm run build` (lokalt eller på VM-en). -2. Kopier innholdet i `dist/` til `/var/www/html/project1/` på - `it2810-36.idi.ntnu.no`. +| State | Hvor | Hva den holder | +| ------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------- | +| `selectedCode` | [`App.tsx`](src/App.tsx#L19) | Landkoden til valgt land, eller `null` før brukeren har valgt noe | +| `sortOrder` | [`useSortOrder.ts`](src/hooks/useSortOrder.ts#L8) | Valgt sortering, med startverdi fra sessionStorage | +| `showOnlyFavorites` | [`useShowOnlyFavorites.ts`](src/hooks/useShowOnlyFavorites.ts#L10) | Om favorittfilteret er på, med startverdi fra sessionStorage | +| `favorites` | [`useFavorites.ts`](src/hooks/useFavorites.ts#L7) | Favorittenes landkoder, med startverdi fra localStorage | +| `isListOpen` | [`CountryMenu.tsx`](src/components/CountryMenu.tsx#L27) | Om landlisten er åpen på smale skjermer (lokal state i komponenten) | +| `failedFlagUrl` | [`CountryCard.tsx`](src/components/CountryCard.tsx#L25) | Flagg-URL-en som ikke kunne lastes, så reserveteksten vises | +| Landdata | [`useCountries.ts`](src/hooks/useCountries.ts) | Data, lastestatus og feil fra TanStack Query | -`base` i `vite.config.ts` er `/project1/`, slik at CSS og JS lastes fra -samme mappe som `index.html`. +De tre hookene for lagrede valg bruker `useState` og lagrer i Web Storage, se [Lagring i nettleseren](#lagring-i-nettleseren). -## Teknologi +**Avledede verdier i stedet for mer state:** Den sorterte listen, det filtrerte utvalget og valgt land regnes ut på nytt ved hver render ([`App.tsx`, linje 29–39](src/App.tsx#L29-L39)). `App` lagrer bare landkoden til valgt land. Finnes ikke koden i utvalget, for eksempel etter filtrering, vises første land. Dermed kan valget aldri bli ugyldig, og det finnes bare én kilde til sannhet. -- React og TypeScript -- Vite -- TanStack Query til henting fra REST-API -- ESLint -- Prettier -- Vanlig CSS +**Eksempel:** Brukeren trykker «Neste». `CountryNavigation` kaller `onSelect` med landkoden til neste land. `App` oppdaterer `selectedCode`, rendres på nytt og sender det nye landet til menyen, navigasjonen og kortet. -## Landdata fra API +### Props -### Datakilde +| Komponent | Props | Rolle | +| -------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- | +| [`CountryMenu`](src/components/CountryMenu.tsx#L5) | `countries`, `selectedCode`, `onSelect`, `isFavorite`, `children` | Sidemeny med landliste. Får sorteringen og filteret som `children`. | +| [`SortControl`](src/components/SortControl.tsx#L8) | `value`, `onChange` | Kontrollert nedtrekksliste for sortering | +| [`FavoritesFilter`](src/components/FavoritesFilter.tsx#L3) | `checked`, `onChange` | Kontrollert avkrysningsboks for favorittfilteret | +| [`CountryNavigation`](src/components/CountryNavigation.tsx#L4) | `countries`, `selectedCode`, `onSelect` | Forrige/neste og posisjon. Regner ut nabolandene fra listen. | +| [`CountryCard`](src/components/CountryCard.tsx#L5) | `country`, `children` | Viser ett land. Får favorittknappen som `children`. | +| [`FavoriteButton`](src/components/FavoriteButton.tsx#L3) | `countryName`, `isFavorite`, `onToggle` | Av/på-knapp for favoritt | -Vi bruker [countries.dev](https://countries.dev) -([dokumentasjon](https://countries.dev/docs)). API-et er gratis, -krever ingen API-nøkkel og tillater kall direkte fra nettleseren (CORS). -REST Countries var førstevalget, men krever nå autentisering. +Alle props har egne TypeScript-typer. `children` brukes til å sette sammen komponentene i [`App.tsx`](src/App.tsx#L93-L123): `CountryMenu` viser sorteringen og filteret uten å vite hvordan de virker, og `CountryCard` viser favorittknappen uten å vite noe om favoritter. -Appen henter dataene fra API-et når den lastes. Vi leverer ingen -forhåndsnedlastet landdatabase, og landdata lagres ikke i -localStorage, sessionStorage eller IndexedDB. Bare brukervalg lagres: -sorteringen og favorittfilteret i sessionStorage, se -[Sortering](#sortering) og [Favorittfilter](#favorittfilter), og -favorittenes landkoder i localStorage, se [Favoritter](#favoritter). +## Lagring i nettleseren -### Datautvalg +| Nøkkel | Lagring | Verdi | Huskes | +| ------------------------------------ | -------------- | --------------------------------------------- | ----------------------------------- | +| `utforsk-verden:sort-order` | sessionStorage | `"name"` eller `"population"` | Ved reload i samme fane | +| `utforsk-verden:show-only-favorites` | sessionStorage | `"true"` eller `"false"` | Ved reload i samme fane | +| `utforsk-verden:favorites` | localStorage | JSON-array med landkoder, som `["BGD","BRA"]` | Også etter at nettleseren er lukket | -Appen gjør én forespørsel: +- **Hvorfor sessionStorage for sortering og filter:** Oppgaven krever at valgene huskes ved reload. sessionStorage gjelder bare fanen, så valgene overlever reload, men en ny fane eller en ny økt starter med standardvalgene. Hver fane kan dermed ha sin egen visning. +- **Hvorfor localStorage for favoritter:** Favorittene skal huskes etter at nettleseren er lukket og startet igjen. Det gjør localStorage, og det gjelder alle faner. +- **Lesing én gang:** Hookene leser lagret verdi som startverdi i `useState` (lazy initializer) og lagrer i funksjonen som endrer verdien, uten `useEffect`. +- **Validering:** Lagrede verdier kontrolleres før de brukes. En ukjent sortering gir standardvalget, bare `"true"` slår på filteret, og ugyldig JSON, verdier som ikke er tekst og duplikater i favorittene hoppes over. +- **Blokkert lagring:** Lesing og skriving ligger i `try/catch` ([`src/utils`](src/utils)). Er lagring blokkert eller full, virker appen, men valgene huskes ikke. +- **Ikke lagret:** landdata (se [Datautvalg](#datautvalg)) og hvilket land som er valgt. -```text -https://countries.dev/countries?fields=alpha3Code,name,flags,capital,region,population,area,languages&sort=population&order=desc&limit=25 -``` +## Responsivt design -- **Antall land:** De 25 mest folkerike landene. Utvalget er avgrenset i - første versjon og settes med `COUNTRY_LIMIT` i `src/api/countries.ts`. - Oseania er foreløpig ikke med i utvalget. -- **Felter:** Bare feltene appen trenger: landkode, navn, flagg, - hovedstad, region, befolkning, areal og språk. -- **Identitet:** Landkoden `alpha3Code` (ISO 3166-1 alpha-3, f.eks. `NOR`) - brukes som identitet og React-key. - -Listeendepunktet returnerer en JSON-array. Svaret gjøres om til typen -`Country` i `src/types/country.ts`. Et svar som ikke er en array, gir en -feil. Land uten kode eller navn hoppes over. Andre felter kan mangle i -API-et, for eksempel hovedstad eller areal, og er derfor valgfrie i typen. - -### Struktur - -| Fil | Ansvar | -| -------------------------------------- | -------------------------------------------------------------------- | -| `src/types/country.ts` | TypeScript-typen for et land | -| `src/api/countries.ts` | `fetchCountries`: henter, kontrollerer og gjør om svaret | -| `src/hooks/useCountries.ts` | `useCountries`: TanStack Query-hook med cache-innstillinger | -| `src/components/CountryCard.tsx` | Landkort som viser ett land, se [Landkort](#landkort) | -| `src/components/CountryCard.css` | Egen styling for landkortet | -| `src/components/CountryMenu.tsx` | Sidemeny med kontroller og landliste, se [Navigasjon](#navigasjon) | -| `src/components/CountryMenu.css` | Egen styling for sidemenyen | -| `src/components/CountryNavigation.tsx` | Forrige/neste og posisjon, se [Navigasjon](#navigasjon) | -| `src/components/CountryNavigation.css` | Egen styling for navigasjonen | -| `src/components/SortControl.tsx` | Valg av sortering, se [Sortering](#sortering) | -| `src/components/SortControl.css` | Egen styling for sorteringsvalget | -| `src/utils/sortCountries.ts` | `sortCountries`, `SortOrder` og validering av sorteringsvalg | -| `src/utils/sortOrderStorage.ts` | Leser og lagrer sorteringsvalget i sessionStorage | -| `src/hooks/useSortOrder.ts` | `useSortOrder`: sorteringsvalg som state, lagret per fane | -| `src/components/FavoriteButton.tsx` | Av/på-knapp for favoritt, se [Favoritter](#favoritter) | -| `src/components/FavoriteButton.css` | Egen styling for favorittknappen | -| `src/utils/favoritesStorage.ts` | Leser, validerer og lagrer favoritter i localStorage | -| `src/hooks/useFavorites.ts` | `useFavorites`: favoritter som state, lagret mellom økter | -| `src/components/FavoritesFilter.tsx` | Av/på-valg for bare favoritter, se [Favorittfilter](#favorittfilter) | -| `src/components/FavoritesFilter.css` | Egen styling for favorittfilteret | -| `src/utils/favoritesFilterStorage.ts` | Leser og lagrer favorittfilteret i sessionStorage | -| `src/hooks/useShowOnlyFavorites.ts` | `useShowOnlyFavorites`: filtervalg som state, lagret per fane | -| `src/App.tsx` | Lasting, feil og tomt resultat, utvalg, valgt land og favoritter | -| `src/main.tsx` | Oppretter én `QueryClient` og `QueryClientProvider` | - -### Hentestrategi med TanStack Query - -- `QueryClient` opprettes én gang utenfor React i `main.tsx`, slik at - cachen ikke nullstilles når komponenter rendres på nytt. -- `useCountries` bruker den faste query key-en `['countries']`. Alle - komponenter som bruker hooken, deler dermed samme data og forespørsel. -- TanStack Query sender med et `AbortSignal`, som `fetchCountries` - sender videre til `fetch`. -- `staleTime` er én time. Landdata endres sjelden, og API-et oppgir - selv `Cache-Control: max-age=3600`. Så lenge dataene er ferske, gir - nye rendringer og nye komponenter ingen nye kall. -- `refetchOnWindowFocus: false` gjør at fanebytte ikke gir nye kall. -- `refetchOnReconnect` og `refetchOnMount` har standardverdien `true`, - men gjelder bare data som er eldre enn `staleTime`. Kommer nettet - tilbake etter mer enn én time, hentes dataene på nytt. Ellers skjer - ingenting. `App` monteres bare én gang, så `refetchOnMount` gir ingen - ekstra kall. -- `retry: 1` gir ett automatisk nytt forsøk, også når brukeren trykker - «Prøv igjen». Deretter vises feilmeldingen. Flere forsøk ville bare - gjort feilmeldingen tregere, og brukeren kan prøve igjen selv. -- `gcTime` har standardverdien fem minutter. Den har ingen praktisk - betydning her, fordi `App` alltid bruker queryen og dataene derfor - aldri fjernes fra cachen mens appen er åpen. -- Cachen ligger bare i minnet og forsvinner ved reload. Da hentes - dataene på nytt, men nettleseren kan bruke sin egen HTTP-cache. -- Sortering, favoritter, favorittfilter, blaing og valg av land skjer - lokalt på dataene som allerede er hentet. De endrer ikke query key-en - og gir derfor ingen nye kall. +Layouten bruker vanlig CSS med grid og flexbox, og media queries i komponentenes CSS-filer. -### Feilhåndtering +| Skjerm | Oppsett | +| ---------------------------- | ------------------------------------------------------------------------- | +| Bredere enn 1024 px | Sidemeny på 260 px til venstre og landkort til høyre, 18 px grunnskrift\* | +| 641–1024 px | Sidemeny på 220 px og 16 px grunnskrift\* | +| 640 px og smalere | Én kolonne. Landlisten ligger bak knappen «Landliste». | +| 480 px og smalere | Etikett og verdi i kortet legges under hverandre | +| Liggende og høyst 500 px høy | Flagget ligger ved siden av faktaene, og overskriften blir mindre | -`fetchCountries` gjør feil om til norske meldinger. `App` viser -«Kunne ikke hente landdata.» etterfulgt av meldingen, og en knapp -«Prøv igjen» som kaller `refetch`. +\*Ved nettleserens standard tekststørrelse. -| Situasjon | Melding | -| ------------------------------------------ | ------------------------------------------------------- | -| Ingen kontakt (nettverks- eller CORS-feil) | «Fikk ikke kontakt med API-et. Sjekk nettforbindelsen.» | -| HTTP-feil, for eksempel 429 eller 500 | «API-et svarte med en feil (HTTP 500).» | -| Svaret er ikke gyldig JSON | «Svaret fra API-et var ikke gyldig JSON.» | -| Svaret er ikke en array | «Svaret fra API-et hadde et uventet format.» | +- På mobil ville 25 land skyve landkortet langt ned, så landlisten er lukket som standard. Knappen har `aria-expanded`, og fokus flyttes til knappen når et land velges. +- Skriftstørrelsene er relative (`%` og `rem`), så teksten følger nettleserens innstilling for tekststørrelse. +- CSS-variabler i `src/index.css` gir kort, meldinger og knapper samme bredde, høyde og hjørner, og egne farger i mørk modus. +- Lange navn og tall brytes over flere linjer, og flagg beholder proporsjonene med `object-fit: contain`. -- Den opprinnelige feilen legges ved som `cause`, slik at den kan - undersøkes ved feilsøking. -- En forespørsel som avbrytes via `AbortSignal`, sendes videre uendret. - TanStack Query håndterer den selv, og den vises ikke som feil. -- **Tomt svar:** `[]` gir «Fant ingen land.». -- **Manglende felter:** Land uten kode eller navn hoppes over. Andre - felter som mangler eller har feil type, blir `undefined`, og kortet - viser «Ikke oppgitt». -- **Feil etter vellykket henting:** Feiler en senere henting i - bakgrunnen, for eksempel ved gjenoppkobling, beholder TanStack Query - de gamle dataene. `App` viser derfor feilsiden bare når det ikke finnes - data, slik at brukeren kan fortsette med landene som allerede er hentet. - -### Utviklingsmodus og produksjonsbygg - -React Strict Mode er beholdt. I utviklingsmodus (`npm run dev`) monterer -Strict Mode komponentene to ganger. Første forespørsel avbrytes da, og en -ny startes. Nettverksfanen viser derfor én avbrutt og én fullført -forespørsel ved oppstart. Dette er forventet og skjer bare i utvikling. -Produksjonsbygget (`npm run build` og `npm run preview`) gjør én -forespørsel. Flere kall i produksjonsbygget ville vært et problem. - -### API-nøkler - -countries.dev krever ingen API-nøkkel. Repoet og klientkoden inneholder -ingen nøkler, tokens eller `.env`-filer. - -### Landkort - -`CountryCard` får ett `Country` via props og henter ingen data selv. -`App` bruker `useCountries`, håndterer lasting, feil og tomt resultat, -og sender valgt land til kortet. - -- **Felter:** Navn, flagg, hovedstad, region, befolkning, areal og språk. - Navn, region og språk vises slik API-et oppgir dem, altså på engelsk. -- **Semantikk:** Kortet er en `
` med landnavnet som `

` - (koblet med `aria-labelledby`). Faktaene ligger i en `
` med - `
`/`
`-par. Flagget har alternativteksten «Flagget til …». -- **Formatering:** Tall formateres med `Intl.NumberFormat('nb-NO')`, - for eksempel `1 402 112 000`. Areal får enheten `km²`. Språk slås - sammen med `Intl.ListFormat`, for eksempel «Hindi og English». -- **Manglende data:** Manglende hovedstad, region, befolkning, areal - eller språk vises som «Ikke oppgitt». Mangler flagget, eller kan - bildet ikke lastes, vises «Flagg ikke tilgjengelig» i stedet. -- **Styling:** Vanlig CSS i `CountryCard.css` med fargevariablene fra - `index.css`, slik at lys og mørk modus fungerer. På smale skjermer - legges etikett og verdi under hverandre. På lave skjermer i liggende - format ligger flagget ved siden av fakta, se - [Responsivt design](#responsivt-design). - -### Navigasjon - -Brukeren kan bla med forrige/neste over kortet og velge land direkte i en -sidemeny. Begge komponentene får landlisten, valgt landkode og en -`onSelect`-funksjon via props. Valgt land og utvalget eies av `App`. - -- **`CountryMenu`:** En `