diff --git a/README.md b/README.md
index 58530e5..a1c2848 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,17 @@
-
# Dokumentasjon
Prosjektet er live på: [http://it2810-02.idi.ntnu.no/project1/](http://it2810-02.idi.ntnu.no/project1/)
## Beskrivelse
+
_Skriv en beskrivelse her_
## oppsett
+
Vi bruker `fnm` med `pnpm`, og `Node.js` v24.
Installasjon kan gjøres med følgende kommandoer (linux):
+
```
# Download and install fnm:
curl -o- https://fnm.vercel.app/install | bash
@@ -25,28 +27,34 @@ corepack enable pnpm
# Verify pnpm version:
pnpm -v
-```
+```
+
+#### Kjøre prosjektet lokalt for utvikling
-#### Kjøre prosjektet lokalt for utvikling
1. Klone github repo til maskinen din
2. gå inn i prosjektmappen (default: T02-Project-1)
3. Kjør kommandoene
- - `pnpm install`
- - `pnpm run dev`
+
+- `pnpm install`
+- `pnpm run dev`
#### Bygge prosjektet
+
Prosjektet kan bygges med kommandoen `pnpm run build`. Dette vil bygge prosjektet og legge det i `/dist` directory under prosjektmappen.
#### Test prosjektet
+
Prosjektet kan testes med kommandoen `pnpm test:run`.
> [!NOTE]
> Prosjektet forventer en api-token i en lokal `.env`-fil. Se _API_-seksjonen under.
## vm
+
Prosjektet er hostet på VM hostet på NTNU sine servere. Oppsettet er gjort i henhold til veiledningen "Komme i gang med virtuell maskin (vm)" på [canvas.ntnu.no](canvas.ntnu.no).
På serveren er det opprettet en `cron job` som kjører hvert minutt som kjører følgende deploy-script:
+
```bash
#!/bin/bash
@@ -82,12 +90,15 @@ echo "[$(date)] Deploy finished."
Prosjektet ligger lokalt i mappen `/var/www/project/`, og prosjektet som blir bygget ligger i `/var/www/project/dist/`. Webserveren hoster filene direkte fra `dist` directory gjennom en symlink i `/var/www/html/project1/` som peker til `dist` directory.
## API
+
Vi bruker The Movie Database (TMDB) sin api i prosjektet vårt. Se tmdb sin egen dokumentasjonen [her](https://developer.themoviedb.org/docs/)
#### VITE_TMDB_TOKEN
+
For autentisering av api-kall mot tmdb sin api kreves det en egen access token. Denne kan etterspørres på under profil-innstillinger på tmdb sin [hjemmeside](https://www.themoviedb.org).
Etter at man har fyllt ut api-skjema får man to forskjellige API nøkkler: en `API key`, og en `API Read Access Token`. Vi bruker `API Read Access Token`. Denne må kopieres og legges i en lokal `.env.local` fil som skal se slik ut:
+
```
VITE_TMDB_TOKEN=[API Read Access Token]
```
@@ -101,8 +112,9 @@ Når man har lagt inn denne skal prosjektet kunne kjøre lokalt, og api-kall ska
## testing
I dette prosjektet bruker vi _Vitest_ med jsdom og React Testing Library. Disse kjøres med:
+
```
-pnpm test
+pnpm test
#watch-modus som kjører blir stående å kjøre tester i bakgrunnen mens du lager nye
@@ -113,49 +125,46 @@ pnpm test:run # 1 gjennomkjørign
Vi har per nå 18 tester frodelt på 5 filer:
-**sortAndFilterMovies.test.ts** er ren logikk uten React. Den tester filtreringen på minmumsvurdering etter alle tre sorteringene og at funksjonen ikke endrer lista den får inn.
+**sortAndFilterMovies.test.ts** er ren logikk uten React. Den tester filtreringen på minmumsvurdering etter alle tre sorteringene og at funksjonen ikke endrer lista den får inn.
-**SeachField.test.tsx** denne tester brukerinteraksjon. Feltet finnes via getByLabelText, som betyr at koblingen mellom htmlFor og id er riktig. Dette er samme kobling en skjermleser bruker. Skrivingen med userEvent bekrefter at feltet er kontrollert. Det melder fra oppover for hvert tastetrykk i stedet for å holde egen state.
+**SeachField.test.tsx** denne tester brukerinteraksjon. Feltet finnes via getByLabelText, som betyr at koblingen mellom htmlFor og id er riktig. Dette er samme kobling en skjermleser bruker. Skrivingen med userEvent bekrefter at feltet er kontrollert. Det melder fra oppover for hvert tastetrykk i stedet for å holde egen state.
-**Sidebar.test.tsx** er for React State. Å klikke på pil ai sidebar endrer både menyen og aria-expanded samtidig, og aria-current settes riktig på favorittsiden.
+**Sidebar.test.tsx** er for React State. Å klikke på pil ai sidebar endrer både menyen og aria-expanded samtidig, og aria-current settes riktig på favorittsiden.
**MovieCard.test.tsx** – props og snapshot. Sjekker at tittel og rating vises, og at favorittknappen kaller tilbake med riktig film-id.
-
-**MovieList.test.tsx** er for mocking. vi.mock bytter ut API-modulen, og testen sjekker i itillegg at global fetch aldri kalles. Dette er slik at vi vet at testene ikke går ut på nettet.
+**MovieList.test.tsx** er for mocking. vi.mock bytter ut API-modulen, og testen sjekker i itillegg at global fetch aldri kalles. Dette er slik at vi vet at testene ikke går ut på nettet.
Testoppsettet ligger som det er nå i src/test/setup.ts . Den inneholder en stub for window.matchMedia som ikke finnes i jsdom. Uten den kr@sjer alt som rendrer Sidebar, fordi useMediaQuery kaller den ved føeste render...
-
### Manuell testing
-
- Testet på:
- - Firefox på macOs for det meste, men har også sjekket at det fungerer på Windows Os og en Ubuntu Os.
-
- - Da nettsiden ikke var lagt ut på serveren før 15. September har vi benyttet oss av Firefox sin innebygd developer tools(Control + Shift + m på windows og option + cmd + m på macOs) for å sjekke mobil view på nettsiden og tilrettelegge etter det. Etter dette har vi nå på 15.september brukt mye tid på manuell testing av nettsiden på telefon, da det er noen ting som man bare oppdager ved faktisk bruk på telefon. Dette har vi opprettet flere issues på git for.
+Testet på:
- - Vi har også satt responsivt oppsett i devtools ned til 320px bredde.
+- Firefox på macOs for det meste, men har også sjekket at det fungerer på Windows Os og en Ubuntu Os.
- Sjekket manuelt:
- - at søk oppdaterer resultater mens man skriver
- - at sortering og filtrering på søkesiden overlever en reload
- - at favoritter overlever at nettleseren lukkes helt
- - at menyen bytter mellom sidefeltet og bunnlinje ved 700px uten en reload
- - detaljvisningen kan åpnes og lukkes både fra kortet og ved å klikke på bakgrunnen
+- Da nettsiden ikke var lagt ut på serveren før 15. September har vi benyttet oss av Firefox sin innebygd developer tools(Control + Shift + m på windows og option + cmd + m på macOs) for å sjekke mobil view på nettsiden og tilrettelegge etter det. Etter dette har vi nå på 15.september brukt mye tid på manuell testing av nettsiden på telefon, da det er noen ting som man bare oppdager ved faktisk bruk på telefon. Dette har vi opprettet flere issues på git for.
- ### Ikke dekket i tester
+- Vi har også satt responsivt oppsett i devtools ned til 320px bredde.
-Vi har ingen ende-til-ende-tester, og layout testes ikke automatisk.
-Responsivt design er kun verifisert manuelt.
-TreningPreview, SortfilterBar og MovieDetails har ingen egne tester som det står nå.
+Sjekket manuelt:
+- at søk oppdaterer resultater mens man skriver
+- at sortering og filtrering på søkesiden overlever en reload
+- at favoritter overlever at nettleseren lukkes helt
+- at menyen bytter mellom sidefeltet og bunnlinje ved 700px uten en reload
+- detaljvisningen kan åpnes og lukkes både fra kortet og ved å klikke på bakgrunnen
+### Ikke dekket i tester
+Vi har ingen ende-til-ende-tester, og layout testes ikke automatisk.
+Responsivt design er kun verifisert manuelt.
+TreningPreview, SortfilterBar og MovieDetails har ingen egne tester som det står nå.
## prosjektstruktur, komponenter, hooks, layout, types
`prosjekt/src`-directory structure:
+
```
./src
├── App.css
@@ -207,10 +216,13 @@ TreningPreview, SortfilterBar og MovieDetails har ingen egne tester som det stå
```
#### components
+
###### MovieCard.tsx + MovieCard.css
+
Viser ett enkelt filmkort med plakat, tittel, rating og skuespillere. Tar imot movieId, title, posterUrl og rating som props. Henter skuespillerne selv via useMovieCredits(movieId) og viser en "Laster skuespillere..."-tekst mens det henter. De tre første skuespillerne vises kommaseparert. Ratingen ligger som en badge øverst i høyre hjørne, plakaten til venstre og resten av infoen til høyre – med responsiv styling for mobil (@media max-width: 480px).
###### MovieList.tsx + MovieList.css
+
Tar imot en liste med filmer (movies: Movie[]) og rendrer ett MovieCard per film med .map(). Den vet ingenting om TMDB eller fetching – det ansvaret ligger i hookene og i Section-komponenten (laster/feilhåndtering). Bygger plakat-URL fra TMDB sin poster_path, med fallback til tom streng hvis filmen mangler plakat. Regner om TMDB sin vote_average (0–10) til prosent (0–100). Layouten er et responsivt grid (repeat(auto-fill, minmax(320px, 1fr))) som går til én kolonne på mobil.
###### Sidebar.tsx + Sidebar.css
@@ -234,10 +246,13 @@ Grunnen til at den finnes er at MovieList eller har måttet håndtere lasting og
Overskriften rendres bare hvis title faktisk har innhold. Banner-seksjonen øverst sendes inn uten tittel, og uten den sjekken ville den laget en tom `
`. Det er ikke bare stygt da tomme overskrifter havner i overskriftoversikten en skjermleser navigerer etter, og da står det bare "overskrifter" uten noe mer.
#### hooks
+
###### useMovieCredits.ts
+
En React Query-hook som henter skuespillerlisten for én bestemt film via fetchMovieCredits i api/tmdb.ts. Caches per movieId i én time (staleTime), slik at samme film ikke hentes på nytt unødvendig når flere kort viser samme data.
###### useSessionStorage.ts
+
En generisk, gjenbrukbar hook som fungerer som `useState`, men som leser fra og skriver til `sessionStorage` automatisk. Brukes til både sorteringsvalg og minimumsvurdering, og kan gjenbrukes til andre ting senere ved behov. Lesing og skriving er pakket inn i try/catch i tilfelle storage er utilgjengelig (f.eks. i privat nettlesing), og faller da tilbake til en gitt standardverdi.
###### useMediaQuery.ts
@@ -251,14 +266,19 @@ Da desktop og mobil har ulik DOM struktur trenger vi dette både i JSX og CSS og
#### types
###### sortAndFilterMovies.ts
+
En ren funksjon uten side-effekter som tar inn en liste med filmer, sorteringsvalg og minimumsvurdering, og returnerer en ny, filtrert og sortert liste. Fordi funksjonen er ren (samme input gir alltid samme output, og den endrer ikke noe utenfor seg selv), er den enkel å enhetsteste isolert med Vitest, uten å måtte mocke React eller komponenter.
###### SortFilterBar.tsx
+
En liten, selvutviklet komponent (ingen tredjepartsbibliotek for UI, i tråd med kravet) med en nedtrekksmeny for sortering og en range-slider for minimumsvurdering. Komponenten er "dum" — den mottar verdier og callbacks som props, og eier ingen egen state selv.
# Vet ikke hvor dette bør stå
+
## Sortering og filtrering
+
Brukeren kan sortere filmlistene (populære, nye filmer og søkeresultater) etter:
+
- Popularitet (TMDB sin standardrekkefølge)
- Vurdering (høyest først)
- Nyeste utgivelsesdato først
@@ -267,14 +287,15 @@ Brukeren kan sortere filmlistene (populære, nye filmer og søkeresultater) ette
I tillegg kan brukeren sette en minimumsgrense for vurdering (0-10) som filtrerer bort filmer under denne terskelen.
sessionStorage og localStorage
+
- Filter-/sorteringsvalg skal huskes **selv om siden reloades
- Favoritter skal huskes **selv om nettleseren avsluttes og startes igjen
-
Kobling i `App.tsx`
`App.tsx` holder sorterings- og filterverdiene via `useSessionStorage`, og sender dem videre til `SortFilterBar` for visning/endring, samt bruker `sortAndFilterMovies(...)` på filmlistene før de sendes videre til `MovieList`. Dermed gjelder valget uansett hvilken liste som vises (populære, nye filmer eller søk).
Kjente begrensninger/mulige forbedringer
+
- Sortering og filtrering kjøres på nytt ved hver rendring. Med dagens datamengde (10–20 filmer) er dette ubetydelig for ytelsen, men kunne vært optimalisert med `useMemo` dersom datamengden økte betydelig.
- Range-slideren for minimumsvurdering kunne fått `aria-valuetext` for bedre skjermleserstøtte (f.eks. "minst 7 av 10" i stedet for kun tallet).
- sessionStorage-nøklene (`sortBy`, `minRating`) er generiske. Ved videre utvikling av appen kan det være lurt å prefikse dem (f.eks. `movieApp:sortBy`) for å unngå navnekollisjon med annen bruk av sessionStorage.
@@ -301,7 +322,9 @@ På tilgjengelighet...
## Bruk av KI
+
#### Leona
+
KI (Claude, Anthropic) ble brukt til å foreslå strukturen på filmkortet: hvilke props komponenten burde ta imot (movieId, title, posterUrl, rating) versus hva den burde hente selv. Forslaget var at kortet henter skuespillerne sine via useMovieCredits(movieId) internt, i stedet for at forelderen henter og sender ned skuespillerlisten som en egen prop. Dette holder MovieList enkel og fri for ansvar den ikke trenger.
KI ble brukt til å foreslå ansvarsdelingen mellom MovieList og resten av appen: MovieList skal kun rendre en liste med MovieCard-komponenter ut fra en ferdig hentet movies-prop, og skal ikke vite noe om TMDB, fetching, lasting eller feilhåndtering — det ansvaret ligger i hookene (useMovies.ts m.fl.) og i Section-komponenten. Denne separasjonen ble foreslått av KI som god praksis for å holde komponentene testbare og gjenbrukbare hver for seg.
@@ -311,6 +334,7 @@ KI foreslo også staleTime: 1000 * 60 * 60 (én time) som en fornuftig cache-var
Løsningen (hook, ren funksjon for sortering/filtrering, UI-komponent og kobling i `App.tsx`) ble utviklet i samarbeid med Claude (Anthropic). KI ble brukt til å foreslå arkitektur (separasjon mellom persistens-hook, ren logikk-funksjon og presentasjonskomponent), generere startkode, og til å vurdere om løsningen faktisk dekket prosjektkravene til filtrering/sortering og bruk av Web Storage API. Koden er gjennomgått og forstått av gruppen før den ble tatt i bruk.
KI ble også brukt til å:
+
1. Foreslå lastetilstand ("Laster skuespillere...") mens useMovieCredits henter data, slik at brukeren får tilbakemelding i stedet for et tomt felt
2. Generere responsiv styling i Movie_card.css, inkludert plassering av rating-badge, layout med plakat til venstre/info til høyre, og tilpasning under @media (max-width: 480px)
3. Gjennomgå og forstått av gruppen før den ble tatt i bruk, og tilpasset til resten av prosjektets struktur (bl.a. typene fra types/tmdb.ts).
@@ -318,7 +342,6 @@ KI ble også brukt til å:
5. Foreslå omregningen fra TMDB sin vote_average (skala 0–10) til prosent (0–100) som vises i UI
6. Sette opp det responsive grid-oppsettet i MovieList.css (repeat(auto-fill, minmax(320px, 1fr))), som går til én kolonne på mobil
-
#### Clemens
#### Albert
diff --git a/index.html b/index.html
index ded5824..a83fc79 100644
--- a/index.html
+++ b/index.html
@@ -1,10 +1,10 @@
-
+
- t02-project-1
+ BetterBoxd
diff --git a/src/App.tsx b/src/App.tsx
index 40fea4f..5c12cbe 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -58,7 +58,6 @@ function App() {
/>
-
{isSearching ? (
{
- onSelectMovie(movie);
- }}
- >
-
-
+
- ★ {(movie.vote_average??0).toFixed(1)}
+ ★ {(movie.vote_average ?? 0).toFixed(1)}
-
{movie.title}
+
+
+
{isLoading && (
Laster skuespillere...
)}
@@ -71,7 +63,14 @@ function MovieCard({
)}
-
+
+
+
);
}
diff --git a/src/components/MovieDetails.css b/src/components/MovieDetails.css
index 1da461a..177d09c 100644
--- a/src/components/MovieDetails.css
+++ b/src/components/MovieDetails.css
@@ -88,9 +88,9 @@
/* Legger til litt css for å gjøre details litt større npår du trykker innpå på telefon. Som det står nå tar
det er bestemt prosent når du åpner, jeg vil gjøre den større, MEN bare på telenfon*/
-@media (max-width:700px) {
+@media (max-width: 700px) {
.details-overlay {
- align-items: flex-end
+ align-items: flex-end;
}
.details-card {
@@ -99,9 +99,9 @@ det er bestemt prosent når du åpner, jeg vil gjøre den større, MEN bare på
height: 92dvh;
margin-bottom: 0.75rem;
}
-
+
.details-backdrop {
- height: 180px
+ height: 180px;
}
.details-close,
@@ -110,4 +110,3 @@ det er bestemt prosent når du åpner, jeg vil gjøre den større, MEN bare på
height: 2.75rem;
}
}
-
diff --git a/src/components/MovieDetails.tsx b/src/components/MovieDetails.tsx
index 3fd928b..d8d1311 100644
--- a/src/components/MovieDetails.tsx
+++ b/src/components/MovieDetails.tsx
@@ -2,6 +2,7 @@ import { useMovieCredits } from '../hooks/useMovieCredits';
import type { Movie } from '../types/tmdb';
import { FavoriteButton } from './FavoriteButton';
import './MovieDetails.css';
+import { useEffect, useRef } from 'react';
type MovieDetailsProps = {
movie: Movie;
@@ -18,19 +19,79 @@ export function MovieDetails({
}: MovieDetailsProps) {
const { data: credits, isLoading } = useMovieCredits(movie.id);
+ const cardRef = useRef(null);
+ const previouslyFocused = useRef(null);
+
+ useEffect(() => {
+ // Må leses før vi flytter fokus, ellers peker den på dialogen selv.
+ previouslyFocused.current = document.activeElement as HTMLElement | null;
+ cardRef.current?.focus();
+
+ return () => {
+ previouslyFocused.current?.focus();
+ };
+ }, []);
+
+ useEffect(() => {
+ function handleKeyDown(e: KeyboardEvent) {
+ if (e.key === 'Escape') {
+ onClose();
+ return;
+ }
+
+ if (e.key !== 'Tab') return;
+
+ const card = cardRef.current;
+ if (!card) return;
+
+ const focusable = card.querySelectorAll(
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
+ );
+ if (focusable.length === 0) return;
+
+ const first = focusable[0];
+ const last = focusable[focusable.length - 1];
+ const active = document.activeElement;
+
+ // aria-modal lover at alt bak dialogen er utilgjengelig, så vi sykler
+ // fokus rundt i stedet for å la Tab vandre ut i sidebaren.
+ // active === card dekker første Shift+Tab, når fokus står på selve
+ // kortet (tabIndex={-1}) og derfor ikke er med i focusable.
+ if (e.shiftKey && (active === first || active === card)) {
+ e.preventDefault();
+ last.focus();
+ } else if (!e.shiftKey && active === last) {
+ e.preventDefault();
+ first.focus();
+ }
+ }
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [onClose]);
+
+ useEffect(() => {
+ document.body.classList.add('modal-open');
+ return () => document.body.classList.remove('modal-open');
+ }, []);
+
return (