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
96 changes: 90 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,97 @@ export default defineConfig([
```

```Dokumentasjon av kode
Leona:
Movie_card.tsx + Movie_card.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.
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.
bruk av KI:
Movie_card.tsx + Movie_card.css
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 også brukt til å:
- Foreslå lastetilstand ("Laster skuespillere...") mens useMovieCredits henter data, slik at brukeren får tilbakemelding i stedet for et tomt felt
- 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)
Koden ble gjennomgått 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).
MovieList.tsx + MovieList.css
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.
KI ble videre brukt til å:
- 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)
- Foreslå omregningen fra TMDB sin vote_average (skala 0–10) til prosent (0–100) som vises i UI
- Sette opp det responsive grid-oppsettet i MovieList.css (repeat(auto-fill, minmax(320px, 1fr))), som går til én kolonne på mobil
Alle forslag ble vurdert og tilpasset av gruppen, blant annet ved å sjekke at key={movie.id} var satt riktig på listeelementene slik React krever.
useMovieCredits.ts
KI ble brukt til å sette opp hooken som en TanStack Query-basert wrapper rundt fetchMovieCredits fra api/tmdb.ts, med movieId som en del av queryKey slik at hver film cacher skuespillerdataen sin uavhengig av andre filmer.
KI foreslo også staleTime: 1000 * 60 * 60 (én time) som en fornuftig cache-varighet, med begrunnelsen at skuespillerlister for en film praktisk talt aldri endrer seg i løpet av en brukerøkt. Dette var spesielt viktig fordi hvert MovieCard gjør sitt eget kall til /credits-endepunktet — uten caching ville samme film blitt hentet på nytt hver gang kortet ble rendret på nytt, noe som kunne gått på bekostning av kravet om å unngå unødvendige API-kall.
Gruppen vurderte selv om caching-strategien var riktig for prosjektets bruksmønster, og valgte å beholde forslaget som det var.
Leona:
Movie_card.tsx + Movie_card.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).
Sortering og filtrering av filmer
Hva er implementert
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
- Alfabetisk (tittel)
I tillegg kan brukeren sette en minimumsgrense for vurdering (0-10) som filtrerer bort filmer under denne terskelen.
Dette dekker det funksjonelle kravet om at brukeren skal kunne gjøre et valg (filtrering/sortering) som påvirker utvalget av det som presenteres og hvordan det presenteres.
Hvorfor sessionStorage (og ikke localStorage)
Prosjektbeskrivelsen skiller mellom to typer valg som skal huskes:
- Filter-/sorteringsvalg skal huskes **selv om siden reloades
- Favoritter skal huskes **selv om nettleseren avsluttes og startes igjen
Dette er to forskjellige persistens-behov, og vi har derfor valgt to forskjellige Web Storage-mekanismer med hensikt:
- sessionStorage for sortering/filtrering - data overlever en reload av siden, men nullstilles når fanen/nettleseren lukkes. Dette er nøyaktig oppførselen kravet beskriver.
- localStorage for favoritter (implementeres separat) - data overlever at nettleseren lukkes helt.
Ved å bruke begge mekanismene til det de faktisk passer til, dekker vi også det tekniske kravet om bruk av *både* localStorage og sessionStorage, i stedet for å bruke én av dem vilkårlig for alt.
Hvordan det er bygget
src/hooks/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.
src/utils/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.
src/components/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.
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).
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.
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.
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.
Bruk av KI
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.
```
16 changes: 14 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { Section } from './components/Section';
import { MovieList } from './components/MovieList';
import { PersonList } from './components/PersonList';
import { useDebouncedValue } from './hooks/useDebouncedValue';

import { useSessionStorage } from './hooks/useSessionStorage';
import { sortAndFilterMovies, type SortOption } from './utils/sortAndFilterMovies';
import { SortFilterBar } from './components/SortFilterBar';
import { useFavorites } from './hooks/useFavorites';
import { TrendingPreview } from './components/TrendingPreview';
import './App.css';
Expand All @@ -14,6 +18,9 @@ function App() {

const [query, setQuery] = useState('');

const [sortBy, setSortBy] = useSessionStorage<SortOption>('sortBy', 'popularity');
const [minRating, setMinRating] = useSessionStorage<number>('minRating', 0);

const popular = usePopMovies()
const current = useCurMovies()
const people = usePopularPeople()
Expand All @@ -26,7 +33,13 @@ function App() {
<div className="page">
<Sidebar onSearch={setQuery} searchValue={query} />

<main>
<main>
<SortFilterBar
sortBy={sortBy}
onSortChange={setSortBy}
minRating={minRating}
onMinRatingChange={setMinRating}
/>
{isSearching ? (
<Section id="sok" title={`Søkeresultater for "${query}"`} isLoading={search.isLoading} error={search.error}>
{search.data && <MovieList movies={search.data} isFavorite={isFavorite} onToggleFavorite={toggleFavorite} />}
Expand All @@ -50,7 +63,6 @@ function App() {
</>
)}
</main>

<div className="page-spacer" aria-hidden="true"></div>
</div>
);
Expand Down
7 changes: 7 additions & 0 deletions src/components/SortFilterBar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.sort-filter-bar {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
align-items: center;
margin-bottom: 1rem;
}
37 changes: 37 additions & 0 deletions src/components/SortFilterBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { SortOption } from '../utils/sortAndFilterMovies';
import './SortFilterBar.css';

type SortFilterBarProps = {
sortBy: SortOption;
onSortChange: (value: SortOption) => void;
minRating: number;
onMinRatingChange: (value: number) => void;
};

export function SortFilterBar({ sortBy, onSortChange, minRating, onMinRatingChange }: SortFilterBarProps) {
return (
<div className="sort-filter-bar">
<label>
Sorter etter:
<select value={sortBy} onChange={(e) => onSortChange(e.target.value as SortOption)}>
<option value="popularity">Popularitet</option>
<option value="rating">Vurdering</option>
<option value="release_date">Nyeste først</option>
<option value="title">Alfabetisk</option>
</select>
</label>

<label>
Min. vurdering: {minRating}
<input
type="range"
min={0}
max={10}
step={1}
value={minRating}
onChange={(e) => onMinRatingChange(Number(e.target.value))}
/>
</label>
</div>
);
}
22 changes: 22 additions & 0 deletions src/hooks/useSessionStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useState, useEffect } from 'react';

export function useSessionStorage<T>(key: string, defaultValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const stored = sessionStorage.getItem(key);
return stored !== null ? (JSON.parse(stored) as T) : defaultValue;
} catch {
return defaultValue;
}
});

useEffect(() => {
try {
sessionStorage.setItem(key, JSON.stringify(value));
} catch {
// f.eks. privat nettlesing der storage kan kaste
}
}, [key, value]);

return [value, setValue] as const;
}
24 changes: 24 additions & 0 deletions src/utils/sortAndFilterMovies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Movie } from '../types/tmdb';

export type SortOption = 'popularity' | 'rating' | 'release_date' | 'title';

export function sortAndFilterMovies(movies: Movie[], sortBy: SortOption, minRating: number): Movie[] {
const filtered = movies.filter((m) => m.vote_average >= minRating);
const sorted = [...filtered];

switch (sortBy) {
case 'rating':
sorted.sort((a, b) => b.vote_average - a.vote_average);
break;
case 'release_date':
sorted.sort((a, b) => (b.release_date ?? '').localeCompare(a.release_date ?? ''));
break;
case 'title':
sorted.sort((a, b) => a.title.localeCompare(b.title));
break;
default:
break; // 'popularity' = behold TMDB sin egen rekkefølge
}

return sorted;
}