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
53 changes: 48 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ 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.
filtervalget huskes etter reload i samme fane. Layouten tilpasser seg
desktop, nettbrett og mobil i både stående og liggende format.

## Krav

Expand Down Expand Up @@ -254,13 +255,15 @@ og sender valgt land til kortet.
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.
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, og eier ingen state selv.
`onSelect`-funksjon via props. Valgt land og utvalget eies av `App`.

- **`CountryMenu`:** En `<aside>` med overskriften «Land». Øverst ligger
sortering og favorittfilter, som menyen tar imot som `children`. Under
Expand All @@ -271,8 +274,8 @@ sidemeny. Begge komponentene får landlisten, valgt landkode og en
- **`CountryNavigation`:** «Forrige», «Land 3 av 25» og «Neste» over
kortet.
- **Oppsett:** Menyen ligger til venstre og valgt land til høyre. Under
768 px ligger menyen over kortet, og landlisten får begrenset høyde
med egen scrolling.
641 px ligger menyen over kortet, og landlisten legges bak knappen
«Landliste (25)», se [Responsivt design](#responsivt-design).
- **Én kilde til valgt land:** `App` lagrer bare landkoden til valgt land
i `useState`. Valgt land slås opp i utvalget, og det samme landet
sendes til menyen, navigasjonen og kortet.
Expand Down Expand Up @@ -404,6 +407,24 @@ tilleggsfunksjon som gjør det enklere å finne favorittene igjen.
- Bare `"true"` slår på filteret. Mangler verdien, er den ugyldig, eller
er lagring blokkert, vises alle land.

### Responsivt design

Layouten bruker vanlig CSS med flexbox og grid.

| Skjerm | Oppsett |
| ---------------------------- | --------------------------------------------------------- |
| Bredere enn 640 px | Sidemeny til venstre (260 px, eller 220 px under 1024 px) |
| 640 px og smalere | Én kolonne, og landlisten ligger bak knappen «Landliste» |
| Liggende og høyst 500 px høy | Flagget ligger ved siden av fakta |

- 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.
- CSS-variabler i `index.css` gir kort, meldinger og knapper samme bredde,
høyde og hjørner.
- Lange navn og tall brytes over flere linjer, og flagg beholder
proporsjonene med `object-fit: contain`.

### Implementert og gjenstående

Implementert:
Expand All @@ -417,6 +438,8 @@ Implementert:
- Sortering etter navn eller befolkning, lagret i sessionStorage.
- Favorittland, lagret i localStorage.
- Filter som viser bare favorittland, lagret i sessionStorage.
- Responsiv layout for desktop, nettbrett og mobil i stående og liggende
format.

Gjenstår i senere issues:

Expand Down Expand Up @@ -597,6 +620,24 @@ ikke lagt i repoet.
ville `App` derfor vist feilsiden. Nå vises dataene videre.
- Ingen JavaScript-feil eller konsollfeil i noen av kjøringene.

### Kontroller av layout

Testet i nettleserens enhetsemulering:

| Størrelse (px) | Tilsvarer |
| --------------------------------- | --------------- |
| 320 × 568 | Liten mobil |
| 375 × 812 | Mobil, stående |
| 667 × 375, 740 × 360 og 812 × 375 | Mobil, liggende |
| 768 × 1024 og 1024 × 768 | Nettbrett |
| 1280 × 800 | Desktop |

- Ingen horisontal scrolling i noen av størrelsene.
- Lange landnavn, mange språk, store tall og manglende flagg ble testet med
midlertidige testdata og brøt ikke layouten.
- Lastetilstand, feilmelding og tomt favorittutvalg passer inn i layouten.
- Ikke testet på ekte mobil eller i Safari ennå.

Automatiske tester som gjenstår når Vitest er satt opp:

- `fetchCountries`: vellykket henting, HTTP-feil, nettverksfeil, ugyldig
Expand All @@ -611,6 +652,8 @@ Automatiske tester som gjenstår når Vitest er satt opp:
med ett land.
- `CountryMenu`: klikk på et land velger det, valgt land har
`aria-current`, favoritter er markert, og tomt utvalg gir melding.
«Landliste»-knappen bytter `aria-expanded`, og valg av land lukker
listen og flytter fokus til knappen.
- `App`: kort og navigasjon viser samme land, og ugyldig eller manglende
valgt kode gir første land.
- `sortCountries`: navn med norske bokstaver, synkende befolkning,
Expand Down
49 changes: 40 additions & 9 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,65 @@
padding: 0 16px 32px;
}

/* Meny til venstre og valgt land til høyre. På smale skjermer under hverandre. */
/* Meny til venstre og valgt land til høyre. Menyen smalner på mellomstore
skjermer, og på smale skjermer ligger den over landet. */
.app__layout {
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
gap: 32px;
align-items: start;
}

@media (max-width: 768px) {
@media (max-width: 1024px) {
.app__layout {
grid-template-columns: minmax(0, 1fr);
grid-template-columns: 220px minmax(0, 1fr);
gap: 24px;
}
}

.country-error button,
.favorites-empty button {
@media (max-width: 640px) {
.app__layout {
grid-template-columns: minmax(0, 1fr);
gap: 16px;
}
}

/* Lastetilstand, feilmelding og tomt resultat får samme bredde og ramme
som landkortet, så de passer inn i layouten. */
.app__message {
width: 100%;
max-width: var(--content-width);
margin: 0 auto;
padding: 24px;
color: var(--text-h);
overflow-wrap: anywhere;
border: 1px dashed var(--border);
border-radius: var(--radius-lg);
}

.app__message button {
min-height: var(--control-height);
margin-top: 12px;
padding: 6px 12px;
padding: 6px 14px;
font: inherit;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
border-radius: 5px;
border-radius: var(--radius);
cursor: pointer;
}

.country-error button:hover,
.favorites-empty button:hover {
.app__message button:hover {
border-color: var(--accent-border);
}

.app__message button:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}

@media (max-width: 480px) {
.app__message {
padding: 16px;
}
}
12 changes: 8 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,25 @@ function App() {

// Ved «Prøv igjen» går queryen tilbake til pending, så lastetilstanden vises igjen.
if (isPending) {
content = <p role="status">Laster land …</p>;
content = (
<p role="status" className="app__message">
Laster land …
</p>
);
} else if (!data) {
// Feilsiden vises bare når det ikke finnes data. Feiler en senere henting
// i bakgrunnen, beholder TanStack Query de gamle dataene, og appen
// fortsetter å vise dem i stedet for å bytte til feilsiden.
content = (
<div role="alert" className="country-error">
<div role="alert" className="app__message">
<p>Kunne ikke hente landdata. {error?.message}</p>
<button type="button" onClick={() => refetch()}>
Prøv igjen
</button>
</div>
);
} else if (sortedCountries.length === 0) {
content = <p>Fant ingen land.</p>;
content = <p className="app__message">Fant ingen land.</p>;
} else {
// Menyen, navigasjonen og kortet bruker samme utvalg og samme land.
content = (
Expand Down Expand Up @@ -114,7 +118,7 @@ function App() {
) : (
// Tomt utvalg er bare mulig når favorittfilteret er på, for eksempel
// uten favoritter eller etter at siste favoritt er fjernet.
<div role="status" className="favorites-empty">
<div role="status" className="app__message">
<p>Du har ingen favorittland å vise.</p>
<button
type="button"
Expand Down
46 changes: 39 additions & 7 deletions src/components/CountryCard.css
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
.country-card {
width: 100%;
max-width: 520px;
max-width: var(--content-width);
margin: 0 auto;
padding: 24px;
box-sizing: border-box;
text-align: left;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 12px;
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}

/* Boksen har fast forhold 3:2, og object-fit: contain gjør at flagg med
andre proporsjoner beholder formen sin i stedet for å strekkes. */
.country-card__flag {
display: block;
width: 100%;
aspect-ratio: 3 / 2;
object-fit: contain;
margin-bottom: 20px;
border-radius: 6px;
border-radius: var(--radius);
background: var(--code-bg);
}

.country-card__flag--missing {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 8px;
text-align: center;
border: 2px dashed var(--border);
color: var(--text);
}
Expand All @@ -39,8 +41,11 @@
margin-bottom: 16px;
}

/* Lange landnavn brytes i stedet for å gjøre kortet bredere. */
.country-card__name {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
}

.country-card__facts {
Expand All @@ -49,7 +54,7 @@

.country-card__facts div {
display: grid;
grid-template-columns: 7rem 1fr;
grid-template-columns: 7rem minmax(0, 1fr);
gap: 12px;
padding: 8px 0;
border-top: 1px solid var(--border);
Expand All @@ -76,7 +81,34 @@
}

.country-card__facts div {
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
gap: 0;
}
}

/* Lav skjerm i liggende format, for eksempel mobil på tvers. Flagget legges
ved siden av fakta, slik at landnavn og fakta synes uten mye scrolling. */
@media (orientation: landscape) and (max-height: 500px) {
.country-card {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
grid-template-rows: auto 1fr;
gap: 0 16px;
padding: 16px;
}

.country-card__flag {
grid-row: 1 / 3;
margin-bottom: 0;
}

.country-card__header {
margin-bottom: 8px;
}

.country-card__facts div {
grid-template-columns: minmax(0, 1fr);
gap: 0;
padding: 4px 0;
}
}
Loading