From f76aa372be9661d2621635c1a52b5b6920b3fa2c Mon Sep 17 00:00:00 2001 From: johannes Date: Thu, 17 Sep 2026 16:32:04 +0200 Subject: [PATCH 1/4] small quick fixes --- index.html | 2 +- src/components/MovieList.tsx | 5 +---- src/components/TrendingPreview.css | 4 ++++ src/components/TrendingPreview.tsx | 8 ++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/index.html b/index.html index ded5824..d5fa4a6 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + diff --git a/src/components/MovieList.tsx b/src/components/MovieList.tsx index 67c9f74..322d3ee 100644 --- a/src/components/MovieList.tsx +++ b/src/components/MovieList.tsx @@ -31,11 +31,8 @@ export function MovieList({ isFavorite={isFavorite(movie.id)} onToggleFavorite={onToggleFavorite} onSelectMovie={onSelectMovie} + key={movie.id} /> - /* - isFavorite={isFavorite(movie.id)} - onToggleFavorite={onToggleFavorite} - */ ))} ); diff --git a/src/components/TrendingPreview.css b/src/components/TrendingPreview.css index 0ef0e66..6404fe4 100644 --- a/src/components/TrendingPreview.css +++ b/src/components/TrendingPreview.css @@ -41,6 +41,10 @@ cursor: pointer; z-index: 3; user-select: none; + background: none; + border: none; + padding: 0; + font: inherit; } .hero-zone-left { diff --git a/src/components/TrendingPreview.tsx b/src/components/TrendingPreview.tsx index 22c93d7..0f78b69 100644 --- a/src/components/TrendingPreview.tsx +++ b/src/components/TrendingPreview.tsx @@ -67,18 +67,18 @@ export function TrendingPreview({ {movie.title} -
handleClick(-1)}> +
+
onSelectMovie(movie)} /> -
handleClick(1)}> +
+
); } From b3b37c38aec4a8382990ad0274fac12d3188765b Mon Sep 17 00:00:00 2001 From: johannes Date: Thu, 17 Sep 2026 17:23:17 +0200 Subject: [PATCH 2/4] =?UTF-8?q?Gj=C3=B8r=20filmkort=20og=20hero-karusell?= =?UTF-8?q?=20tastaturnavigerbare?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 +- src/components/MovieCard.css | 25 +++++++++++++++++- src/components/MovieCard.tsx | 25 +++++++++--------- src/components/TrendingPreview.css | 2 +- src/components/TrendingPreview.tsx | 12 +++++---- .../__snapshots__/MovieCard.test.tsx.snap | 26 +++++++++++-------- 6 files changed, 60 insertions(+), 32 deletions(-) diff --git a/index.html b/index.html index d5fa4a6..a83fc79 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - t02-project-1 + BetterBoxd
diff --git a/src/components/MovieCard.css b/src/components/MovieCard.css index 00298a2..0d1a76d 100644 --- a/src/components/MovieCard.css +++ b/src/components/MovieCard.css @@ -49,12 +49,35 @@ font-size: 1.6rem; font-weight: 700; line-height: 1.2; - overflow: hidden; text-overflow: ellipsis; +} + +.movie-card__title button { + font: inherit; + color: inherit; + text-align: left; + cursor: pointer; + background: none; + border: none; + padding: 0; display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; + overflow: hidden; +} +.movie-card__title button::after { + content: ''; + position: absolute; + inset: 0; +} + +.movie-card:has(.movie-card__title button:focus-visible){ + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.movie-card__title button:focus-visible { + outline: none; } .movie-card__actors { diff --git a/src/components/MovieCard.tsx b/src/components/MovieCard.tsx index bd2bea9..04d3dc4 100644 --- a/src/components/MovieCard.tsx +++ b/src/components/MovieCard.tsx @@ -42,26 +42,18 @@ function MovieCard({ : ''; return ( -
{ - onSelectMovie(movie); - }} > - + ★ {(movie.vote_average??0).toFixed(1)} - {movie.title} + {movie.title}/
-

{movie.title}

+

{isLoading && (

Laster skuespillere...

)} @@ -71,7 +63,14 @@ function MovieCard({

)}
-
+ + + ); } diff --git a/src/components/TrendingPreview.css b/src/components/TrendingPreview.css index 6404fe4..954c301 100644 --- a/src/components/TrendingPreview.css +++ b/src/components/TrendingPreview.css @@ -44,7 +44,7 @@ background: none; border: none; padding: 0; - font: inherit; + font: inherit } .hero-zone-left { diff --git a/src/components/TrendingPreview.tsx b/src/components/TrendingPreview.tsx index 0f78b69..34c6f72 100644 --- a/src/components/TrendingPreview.tsx +++ b/src/components/TrendingPreview.tsx @@ -55,28 +55,30 @@ export function TrendingPreview({ - + -

onSelectMovie(movie)}> +

{movie.title}

- -
onSelectMovie(movie)} /> -
diff --git a/src/components/__snapshots__/MovieCard.test.tsx.snap b/src/components/__snapshots__/MovieCard.test.tsx.snap index ff1bd00..9564bc3 100644 --- a/src/components/__snapshots__/MovieCard.test.tsx.snap +++ b/src/components/__snapshots__/MovieCard.test.tsx.snap @@ -2,17 +2,9 @@ exports[`MovieCard > matcher snapshot 1`] = `
-
- @@ -29,7 +21,11 @@ exports[`MovieCard > matcher snapshot 1`] = `

- Testfilm +

matcher snapshot 1`] = ` Skuespiller En, Skuespiller To, Skuespiller Tre

-
+ + `; From 1dc819db35d493a67bc2437dae9d08cdbee94328 Mon Sep 17 00:00:00 2001 From: johannes Date: Thu, 17 Sep 2026 17:52:50 +0200 Subject: [PATCH 3/4] =?UTF-8?q?Gj=C3=B8r=20movie-details=20tastaturnaviger?= =?UTF-8?q?bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/MovieDetails.css | 3 ++- src/components/MovieDetails.tsx | 32 +++++++++++++++++++++++------- src/components/TrendingPreview.css | 18 +++++++++++++++++ src/index.css | 6 ++++++ 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/components/MovieDetails.css b/src/components/MovieDetails.css index 34cf23e..9fcd504 100644 --- a/src/components/MovieDetails.css +++ b/src/components/MovieDetails.css @@ -85,6 +85,7 @@ cursor: pointer; } + /* 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*/ @@ -104,7 +105,7 @@ det er bestemt prosent når du åpner, jeg vil gjøre den større, MEN bare på height: 180px } - .details-close + .details-close, .details-favorite { width: 2.75rem; height: 2.75rem; diff --git a/src/components/MovieDetails.tsx b/src/components/MovieDetails.tsx index 3fd928b..70b87da 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,28 +19,45 @@ export function MovieDetails({ }: MovieDetailsProps) { const { data: credits, isLoading } = useMovieCredits(movie.id); +const cardRef = useRef(null); + +useEffect(() => { + cardRef.current?.focus(); +}, []); + +useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') onClose(); + } + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); +}, [onClose]); + return (
-
e.stopPropagation()}> +
e.stopPropagation()} ref={cardRef} tabIndex={-1}> {movie.title} - - - + + + +
-

{movie.title}

+

{movie.title}

{movie.release_date} · ★ {movie.vote_average.toFixed(1)}

diff --git a/src/components/TrendingPreview.css b/src/components/TrendingPreview.css index 954c301..a761b8d 100644 --- a/src/components/TrendingPreview.css +++ b/src/components/TrendingPreview.css @@ -74,3 +74,21 @@ .hero-zone:hover .hero-arrow { opacity: 1; } + +.hero-zone:focus-visible { + outline: none; +} + +.hero-zone:focus-visible .hero-arrow { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.6); + opacity: 1; +} + +.hero:has(.hero-zone-middle:focus-visible) .hero-title { + outline: 2px solid var(--accent); + outline-offset: 4px; + border-radius: 0.25rem; +} diff --git a/src/index.css b/src/index.css index 8cb6ea6..8cd3cf5 100644 --- a/src/index.css +++ b/src/index.css @@ -107,3 +107,9 @@ code { padding: 4px 8px; background: var(--code-bg); } + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + +} From 6f31b8683aa2bf235a8a89b40407919d7ead6daf Mon Sep 17 00:00:00 2001 From: johannes Date: Thu, 17 Sep 2026 18:15:54 +0200 Subject: [PATCH 4/4] =?UTF-8?q?Gj=C3=B8r=20movie-details=20fokussikker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 87 ++++++++++++++++++------------ src/App.tsx | 1 - src/components/MovieCard.css | 12 ++--- src/components/MovieCard.tsx | 16 +++--- src/components/MovieDetails.css | 12 ++--- src/components/MovieDetails.tsx | 86 +++++++++++++++++++++++------ src/components/TrendingPreview.css | 8 +-- src/components/TrendingPreview.tsx | 28 +++++++--- src/index.css | 5 +- src/layout/FavoritesLayout.tsx | 2 - src/layout/MainLayout.tsx | 1 - 11 files changed, 171 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index ba7ea8c..7b41f2d 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,33 +216,43 @@ 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. #### 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. +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. #### layout #### 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 @@ -242,22 +261,23 @@ 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. - - ## 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. @@ -267,9 +287,10 @@ 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). 4. Generere logikken for å bygge full plakat-URL fra TMDB sin poster_path, inkludert fallback til tom streng når poster_path er null (for å unngå en ødelagt bilde-URL som .../w200null) 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 \ No newline at end of file +6. Sette opp det responsive grid-oppsettet i MovieList.css (repeat(auto-fill, minmax(320px, 1fr))), som går til én kolonne på mobil 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 ? ( - - +
- ★ {(movie.vote_average??0).toFixed(1)} + ★ {(movie.vote_average ?? 0).toFixed(1)} - {movie.title}/ + {movie.title}
-

+

+ +

{isLoading && (

Laster skuespillere...

)} diff --git a/src/components/MovieDetails.css b/src/components/MovieDetails.css index 9fcd504..177d09c 100644 --- a/src/components/MovieDetails.css +++ b/src/components/MovieDetails.css @@ -85,13 +85,12 @@ cursor: pointer; } - /* 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 { @@ -100,15 +99,14 @@ 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, + .details-close, .details-favorite { width: 2.75rem; height: 2.75rem; } } - diff --git a/src/components/MovieDetails.tsx b/src/components/MovieDetails.tsx index 70b87da..d8d1311 100644 --- a/src/components/MovieDetails.tsx +++ b/src/components/MovieDetails.tsx @@ -19,43 +19,95 @@ export function MovieDetails({ }: MovieDetailsProps) { const { data: credits, isLoading } = useMovieCredits(movie.id); -const cardRef = useRef(null); + const cardRef = useRef(null); + const previouslyFocused = useRef(null); -useEffect(() => { - cardRef.current?.focus(); -}, []); + useEffect(() => { + // Må leses før vi flytter fokus, ellers peker den på dialogen selv. + previouslyFocused.current = document.activeElement as HTMLElement | null; + cardRef.current?.focus(); -useEffect(() => { - function handleKeyDown(e: KeyboardEvent) { - if (e.key === 'Escape') onClose(); - } - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); -}, [onClose]); + 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 (
-
e.stopPropagation()} ref={cardRef} tabIndex={-1}> +
e.stopPropagation()} + ref={cardRef} + tabIndex={-1} + > {movie.title} - - - -

{movie.title}

diff --git a/src/components/TrendingPreview.css b/src/components/TrendingPreview.css index a761b8d..26d52f2 100644 --- a/src/components/TrendingPreview.css +++ b/src/components/TrendingPreview.css @@ -41,10 +41,10 @@ cursor: pointer; z-index: 3; user-select: none; - background: none; - border: none; - padding: 0; - font: inherit + background: none; + border: none; + padding: 0; + font: inherit; } .hero-zone-left { diff --git a/src/components/TrendingPreview.tsx b/src/components/TrendingPreview.tsx index 34c6f72..cdf736f 100644 --- a/src/components/TrendingPreview.tsx +++ b/src/components/TrendingPreview.tsx @@ -57,7 +57,11 @@ export function TrendingPreview({ className="backdrop" alt="" /> - +

-

- {movie.title} -

+

{movie.title}

- -
diff --git a/src/index.css b/src/index.css index 8cd3cf5..80672e4 100644 --- a/src/index.css +++ b/src/index.css @@ -111,5 +111,8 @@ code { :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; - +} + +body.modal-open { + overflow: hidden; } diff --git a/src/layout/FavoritesLayout.tsx b/src/layout/FavoritesLayout.tsx index e80177d..e8845bb 100644 --- a/src/layout/FavoritesLayout.tsx +++ b/src/layout/FavoritesLayout.tsx @@ -3,7 +3,6 @@ import { MovieList } from '../components/MovieList'; import { useFavoriteMovies } from '../hooks/useMovies'; import type { Movie } from '../types/tmdb'; - type FavoritesLayoutProps = { favoriteIds: number[]; isFavorite: (id: number) => boolean; @@ -18,7 +17,6 @@ export function FavoritesLayout({ onSelectMovie, }: FavoritesLayoutProps) { const { movies, isLoading, error } = useFavoriteMovies(favoriteIds); - return (
boolean; toggleFavorite: (id: number) => void;