diff --git a/FrameRate/.env.example b/FrameRate/.env.example new file mode 100644 index 0000000..0f7b534 --- /dev/null +++ b/FrameRate/.env.example @@ -0,0 +1,4 @@ +# Kopier til .env.local for å endre innstillinger lokalt. .env.local committes ikke. + +# Adressen til GraphQL-backenden. Standard er backenden på egen maskin. +VITE_GRAPHQL_URL=http://localhost:3001/ diff --git a/FrameRate/package-lock.json b/FrameRate/package-lock.json index d00450f..cbbef43 100644 --- a/FrameRate/package-lock.json +++ b/FrameRate/package-lock.json @@ -8,8 +8,11 @@ "name": "framerate", "version": "0.0.0", "dependencies": { + "@apollo/client": "^4.3.5", + "graphql": "^16.14.2", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "rxjs": "^7.8.2" }, "devDependencies": { "@babel/core": "^7.29.7", @@ -35,6 +38,43 @@ "vitest": "^5.0.3" } }, + "node_modules/@apollo/client": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.3.5.tgz", + "integrity": "sha512-phA1B82EisAHIjT6irj9Lm4I5516pASExgACUl8pzcnq5eGmNjBhImDgjSRHRt18u1Hrqw0uP71jKJtEjV/Fag==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", + "@wry/equality": "^0.5.6", + "@wry/trie": "^0.5.0", + "graphql-tag": "^2.12.6", + "optimism": "^0.18.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "graphql": "^16.0.0 || ^17.0.0", + "graphql-ws": "^5.5.5 || ^6.0.3", + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "react-dom": "^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "rxjs": "^7.3.0", + "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "subscriptions-transport-ws": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -505,6 +545,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1373,6 +1422,54 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@wry/caches": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", + "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/context": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", + "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/equality": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", + "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/trie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz", + "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/acorn": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", @@ -2890,6 +2987,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphql": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-tag": { + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.7.tgz", + "integrity": "sha512-xnE/NFzy+0eIesvAsREJZ284zTl/wYuBAvpsFSDhRGRdRHdnE90M21Q3xAWyYInb0J756c6x0pIQ62+vtvOs1Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4067,6 +4188,18 @@ "node": ">=12.20.0" } }, + "node_modules/optimism": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz", + "integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==", + "license": "MIT", + "dependencies": { + "@wry/caches": "^1.0.0", + "@wry/context": "^0.7.0", + "@wry/trie": "^0.5.0", + "tslib": "^2.3.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4386,6 +4519,15 @@ "@rolldown/binding-win32-x64-msvc": "1.2.12" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", @@ -4804,6 +4946,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/FrameRate/package.json b/FrameRate/package.json index ab4dade..712905d 100644 --- a/FrameRate/package.json +++ b/FrameRate/package.json @@ -12,8 +12,11 @@ "format:check": "prettier --check ." }, "dependencies": { + "@apollo/client": "^4.3.5", + "graphql": "^16.14.2", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "rxjs": "^7.8.2" }, "devDependencies": { "@babel/core": "^7.29.7", diff --git a/FrameRate/src/App.tsx b/FrameRate/src/App.tsx index d80a585..8404aca 100644 --- a/FrameRate/src/App.tsx +++ b/FrameRate/src/App.tsx @@ -1,11 +1,13 @@ import './index.css'; import { Layout } from './components/layout/Layout'; import { FeaturedMovie } from './components/FeaturedMovie'; +import { MovieSearch } from './components/search/MovieSearch'; function App() { return ( + ); } diff --git a/FrameRate/src/apollo/client.ts b/FrameRate/src/apollo/client.ts new file mode 100644 index 0000000..79dc672 --- /dev/null +++ b/FrameRate/src/apollo/client.ts @@ -0,0 +1,14 @@ +import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'; + +// Adressen til GraphQL-backenden. Settes i .env.local (lokalt) eller ved bygging for VM-en, +// se .env.example. Uten innstilling brukes backenden på egen maskin. +const GRAPHQL_URL = import.meta.env.VITE_GRAPHQL_URL ?? 'http://localhost:3001/'; + +/** + * Apollo Client henter data fra backenden og fungerer også som appens state management: + * svarene lagres i en cache, så samme spørring ikke sendes på nytt når brukeren går frem og tilbake. + */ +export const client = new ApolloClient({ + link: new HttpLink({ uri: GRAPHQL_URL }), + cache: new InMemoryCache(), +}); diff --git a/FrameRate/src/components/FeaturedMovie.tsx b/FrameRate/src/components/FeaturedMovie.tsx index c60e632..9a44d7e 100644 --- a/FrameRate/src/components/FeaturedMovie.tsx +++ b/FrameRate/src/components/FeaturedMovie.tsx @@ -1,42 +1,69 @@ -import { featuredMovie as movie, featuredReview as review, genres } from '../data/mockData'; +import { useQuery } from '@apollo/client/react'; +import { SEARCH_MOVIES, posterUrl } from '../graphql/movies'; -const IMAGE_BASE = 'https://image.tmdb.org/t/p'; const DICE = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅']; +/** + * Filmen med høyest terningkast fra våre egne anmeldelser. + * Før noen har anmeldt noe, velger backenden den mest populære filmen i stedet. + */ export function FeaturedMovie() { - const year = movie.release_date.slice(0, 4); - const genreNames = movie.genre_ids.map(id => genres[id]); - const rounded = Math.round(movie.average_rating); + const { data, loading, error } = useQuery(SEARCH_MOVIES, { + variables: { sort: 'RATING_DESC', limit: 1 }, + }); + + const movie = data?.searchMovies.movies[0]; + + if (loading) { + return ( +
+

Henter utvalgt film …

+
+ ); + } + + if (error || !movie) { + return ( +
+

Kunne ikke hente utvalgt film akkurat nå.

+
+ ); + } + + const rating = movie.averageRating; return (
{/* Venstre: tekst */}
-

Utvalgt fra topplisten

+

+ {rating != null ? 'Utvalgt fra topplisten' : 'Utvalgt film'} +

{movie.title}

- {year} - {genreNames.join(' / ')} + {movie.releaseYear && {movie.releaseYear}} + {movie.genres.map(g => g.name).join(' / ')}

- + {rating != null && ( + + )}
-

Terningkast {movie.average_rating.toFixed(1)}

-

Basert på {movie.review_count} anmeldelser

+

+ {rating != null ? `Terningkast ${rating.toFixed(1)}` : 'Ingen anmeldelser ennå'} +

+

+ {rating != null + ? `Basert på ${movie.reviewCount} anmeldelser` + : 'Bli den første som gir terningkast!'} +

-
-

«{review.text}»

-
- — {review.author}, {review.date} -
-
-
Les mer om filmen @@ -48,13 +75,15 @@ export function FeaturedMovie() {
{/* Høyre: plakat */} - {`Plakat + {movie.posterPath && ( + {`Plakat + )}
); } diff --git a/FrameRate/src/components/search/MovieList.tsx b/FrameRate/src/components/search/MovieList.tsx new file mode 100644 index 0000000..c31d067 --- /dev/null +++ b/FrameRate/src/components/search/MovieList.tsx @@ -0,0 +1,44 @@ +import { posterUrl, type MovieSummary } from '../../graphql/movies'; + +interface MovieListProps { + movies: MovieSummary[]; + /** true mens nye resultater lastes, så lista kan tones ned. */ + busy?: boolean; +} + +/** Liste med filmkort: plakat, tittel, år, sjangre og terningkast. */ +export function MovieList({ movies, busy = false }: MovieListProps) { + return ( + + ); +} diff --git a/FrameRate/src/components/search/MovieSearch.tsx b/FrameRate/src/components/search/MovieSearch.tsx new file mode 100644 index 0000000..dcf5f5b --- /dev/null +++ b/FrameRate/src/components/search/MovieSearch.tsx @@ -0,0 +1,62 @@ +import { useId, useState } from 'react'; +import { useQuery } from '@apollo/client/react'; +import { SEARCH_MOVIES } from '../../graphql/movies'; +import { useDebouncedValue } from '../../hooks/useDebouncedValue'; +import { MovieList } from './MovieList'; + +const PAGE_SIZE = 20; + +/** Søkefelt med resultatliste. Søket går mot vår egen database via GraphQL. */ +export function MovieSearch() { + const inputId = useId(); + const [input, setInput] = useState(''); + // Venter 300 ms etter siste tastetrykk før vi søker, så vi ikke sender en forespørsel per bokstav. + const search = useDebouncedValue(input.trim(), 300); + + const { data, previousData, loading, error } = useQuery(SEARCH_MOVIES, { + variables: { search, limit: PAGE_SIZE, offset: 0 }, + skip: search === '', + }); + + // Mens et nytt søk lastes, vises forrige resultat i stedet for at lista blinker tom. + const result = (data ?? previousData)?.searchMovies; + + return ( +
+

+ Søk etter filmer +

+ + + setInput(e.target.value)} + autoComplete="off" + /> + + {/* aria-live gjør at skjermlesere leser opp antall treff når det endrer seg. */} +

+ {search === '' + ? '' + : error + ? 'Kunne ikke hente filmer. Prøv igjen senere.' + : loading && !result + ? 'Søker …' + : result && + (result.totalCount === 0 + ? `Ingen filmer funnet for «${search}». Prøv et annet ord.` + : `${result.totalCount} treff for «${search}»`)} +

+ + {search !== '' && result && result.movies.length > 0 && ( + + )} +
+ ); +} diff --git a/FrameRate/src/data/mockData.ts b/FrameRate/src/data/mockData.ts deleted file mode 100644 index faf7b09..0000000 --- a/FrameRate/src/data/mockData.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Midlertidige data i samme format som TMDB. -// Byttes ut med data fra vår egen GraphQL-backend senere. - -// Sjangernavn for genre_ids. Hentes fra TMDB /genre/movie/list ved import. -export const genres: Record = { - 28: 'Action', - 18: 'Drama', - 80: 'Krim', - 9648: 'Mysterium', - 53: 'Thriller', -}; - -export const featuredMovie = { - // --- Fra TMDB --- - id: 343611, - title: 'Jack Reacher: Never Go Back', - original_title: 'Jack Reacher: Never Go Back', - original_language: 'en', - overview: - 'Jack Reacher must uncover the truth behind a major government conspiracy in order to clear his name. On the run as a fugitive from the law, Reacher uncovers a potential secret from his past that could change his life forever.', - release_date: '2016-10-19', - genre_ids: [53, 28, 80, 18, 9648], - poster_path: '/IfB9hy4JH1eH6HEfIgIGORXi5h.jpg', - backdrop_path: '/4ynQYtSEuU5hyipcGkfD6ncwtwz.jpg', - popularity: 26.818468, - vote_average: 4.19, - vote_count: 201, - adult: false, - video: false, - - // --- Fra /movie/{id} ved import --- - runtime: 118, - director: 'Edward Zwick', - - // --- Fra våre egne anmeldelser --- - average_rating: 4.3, - review_count: 12, -}; - -export const featuredReview = { - text: 'Solid action, men historien er mer forutsigbar enn den første filmen.', - author: 'henrik', - date: '24. september 2026', -}; diff --git a/FrameRate/src/graphql/movies.ts b/FrameRate/src/graphql/movies.ts new file mode 100644 index 0000000..d697807 --- /dev/null +++ b/FrameRate/src/graphql/movies.ts @@ -0,0 +1,87 @@ +import { gql } from '@apollo/client'; +import type { TypedDocumentNode } from '@apollo/client'; + +// GraphQL-spørringer for filmer, med TypeScript-typer for svarene. +// Feltene må stemme med backend/src/schema.ts. + +export type MovieSort = + 'POPULARITY_DESC' | 'RATING_DESC' | 'RELEASE_DESC' | 'RELEASE_ASC' | 'TITLE_ASC'; + +/** Feltene vi trenger for å vise en film i en liste eller på forsiden. */ +export interface MovieSummary { + id: number; + title: string; + releaseYear: number | null; + posterPath: string | null; + averageRating: number | null; + reviewCount: number; + genres: { id: number; name: string }[]; +} + +export interface SearchMoviesData { + searchMovies: { + totalCount: number; + movies: MovieSummary[]; + }; +} + +export interface SearchMoviesVariables { + search?: string; + genreIds?: number[]; + yearFrom?: number; + yearTo?: number; + minRating?: number; + sort?: MovieSort; + limit?: number; + offset?: number; +} + +const MOVIE_SUMMARY_FIELDS = gql` + fragment MovieSummaryFields on Movie { + id + title + releaseYear + posterPath + averageRating + reviewCount + genres { + id + name + } + } +`; + +export const SEARCH_MOVIES: TypedDocumentNode = gql` + ${MOVIE_SUMMARY_FIELDS} + query SearchMovies( + $search: String + $genreIds: [Int!] + $yearFrom: Int + $yearTo: Int + $minRating: Float + $sort: MovieSort + $limit: Int + $offset: Int + ) { + searchMovies( + search: $search + genreIds: $genreIds + yearFrom: $yearFrom + yearTo: $yearTo + minRating: $minRating + sort: $sort + limit: $limit + offset: $offset + ) { + totalCount + movies { + ...MovieSummaryFields + } + } + } +`; + +/** Plakat-URL hos TMDB. Bruk minste størrelse som ser bra ut, det sparer data (bærekraft). */ +export function posterUrl(posterPath: string, size: 'w92' | 'w185' | 'w342' | 'w500') { + return `https://image.tmdb.org/t/p/${size}${posterPath}`; +} diff --git a/FrameRate/src/hooks/useDebouncedValue.ts b/FrameRate/src/hooks/useDebouncedValue.ts new file mode 100644 index 0000000..b487f1d --- /dev/null +++ b/FrameRate/src/hooks/useDebouncedValue.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +/** + * Gir tilbake `value`, men først når den har stått uendret i `delayMs` millisekunder. + * Brukes i søket så vi ikke sender en forespørsel for hvert tastetrykk. + */ +export function useDebouncedValue(value: T, delayMs = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delayMs); + // Skriver brukeren igjen før tiden er ute, avbrytes den forrige timeren. + return () => clearTimeout(timer); + }, [value, delayMs]); + + return debounced; +} diff --git a/FrameRate/src/index.css b/FrameRate/src/index.css index d242f05..1add0a5 100644 --- a/FrameRate/src/index.css +++ b/FrameRate/src/index.css @@ -373,6 +373,88 @@ body { background: rgba(242, 182, 48, 0.1); } +/* ---------- Søk ---------- */ +.search { + max-width: 75rem; + padding: 2rem 1.5rem 4rem; +} + +.search-heading { + margin: 0 0 1rem; + font-family: Georgia, serif; + font-size: 2rem; +} + +.search-input { + box-sizing: border-box; + width: 100%; + max-width: 36rem; + padding: 0.8rem 1rem; + border: 1px solid #3a3e48; + border-radius: 8px; + background: #15171c; + color: white; + font: inherit; + font-size: 1.1rem; +} + +.search-input::placeholder { + color: #8a8f99; +} + +.search-status { + min-height: 1.5em; /* holder plassen, så lista ikke hopper når teksten dukker opp */ + color: #a0a4ad; +} + +/* ---------- Filmkort ---------- */ +.movie-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); + gap: 1.5rem; + margin: 1rem 0 0; + padding: 0; + list-style: none; + transition: opacity 0.2s; +} + +.movie-list[aria-busy='true'] { + opacity: 0.6; +} + +.movie-card-link { + display: block; + color: white; + text-decoration: none; +} + +.movie-card-link:hover .movie-card-title { + color: #f2b630; +} + +.movie-card-poster { + display: block; + width: 100%; + height: auto; + aspect-ratio: 2 / 3; + border-radius: 8px; + background: #15171c; + object-fit: cover; +} + +.movie-card-title { + display: block; + margin-top: 0.5rem; + font-weight: bold; +} + +.movie-card-meta, +.movie-card-rating { + margin: 0.25rem 0 0; + color: #a0a4ad; + font-size: 0.9rem; +} + /* ---------- Footer ---------- */ .footer { padding: 2rem 1.5rem; @@ -437,11 +519,22 @@ body { /* ---------- Tastaturfokus ---------- */ a:focus-visible, -button:focus-visible { +button:focus-visible, +input:focus-visible { outline: 3px solid #f2b630; outline-offset: 3px; } +/* Skjult på skjermen, men lest opp av skjermlesere (f.eks. etiketter på skjemafelt). */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + /* ---------- Mindre bevegelse ---------- */ @media (prefers-reduced-motion: reduce) { .reel, @@ -454,4 +547,8 @@ button:focus-visible { .header-nav:focus-within .spotlight { animation: none; } + + .movie-list { + transition: none; + } } diff --git a/FrameRate/src/main.tsx b/FrameRate/src/main.tsx index df655ea..e636c4a 100644 --- a/FrameRate/src/main.tsx +++ b/FrameRate/src/main.tsx @@ -1,10 +1,14 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { ApolloProvider } from '@apollo/client/react'; +import { client } from './apollo/client'; import './index.css'; import App from './App.tsx'; createRoot(document.getElementById('root')!).render( - + + + ); diff --git a/backend/src/resolvers.ts b/backend/src/resolvers.ts index 184058f..d0a4a44 100644 --- a/backend/src/resolvers.ts +++ b/backend/src/resolvers.ts @@ -33,7 +33,8 @@ const MAX_LIMIT = 50; // m.id til slutt gjør rekkefølgen stabil, så "Vis flere" ikke gir duplikater. const ORDER_BY: Record = { POPULARITY_DESC: 'm.popularity DESC, m.id', - RATING_DESC: 'm.average_rating DESC NULLS LAST, m.review_count DESC, m.id', + // Likt terningkast (eller ingen anmeldelser ennå): flest anmeldelser, så mest populær. + RATING_DESC: 'm.average_rating DESC NULLS LAST, m.review_count DESC, m.popularity DESC, m.id', RELEASE_DESC: 'm.release_date DESC NULLS LAST, m.id', RELEASE_ASC: 'm.release_date ASC NULLS LAST, m.id', TITLE_ASC: 'm.title ASC, m.id', diff --git a/docs/AI_Deklarasjon/AI_Deklarasjon.md b/docs/AI_Deklarasjon/AI_Deklarasjon.md index a48d097..25351d4 100644 --- a/docs/AI_Deklarasjon/AI_Deklarasjon.md +++ b/docs/AI_Deklarasjon/AI_Deklarasjon.md @@ -62,3 +62,19 @@ Her dokumenterer vi bruk av KI-verktøy i prosjektet, med én del per pull reque - Bestemt hva guiden skal inneholde og hvem den er for (utviklere, ikke vanlige brukere), og at gruppas nøkkel ikke skal deles - Testet stegene på macOS + +## PR #24 – Apollo Client, søk og ekte film på forsiden (`feat/apollo-search`) + +**Verktøy:** Claude Code +**Brukt av:** Akhil + +### Hva KI ble brukt til + +- Fikk hjelp til å kode med Claude Code. Den forklarte også koden underveis og en del ting jeg ikke kunne fra før, som Apollo Client, debounce og hvordan man gjør søk tilgjengelig for skjermlesere +- Brukt til oppsett av Apollo Client (#6), søket (#8) og å koble forsiden til backenden (#7), pluss CSS for søket + +### Hva jeg har gjort selv + +- Valgte hvilke issues vi skulle ta og i hvilken rekkefølge +- Gikk gjennom gjøremålene i hver issue og sjekket at alt var med +- Testet alt i nettleseren