From e944c146944eead169e3259d4b2834ca34216432 Mon Sep 17 00:00:00 2001 From: Clemens Beese Date: Thu, 22 Oct 2026 14:44:27 +0200 Subject: [PATCH 1/9] chore: JWT_SECRET i .env.example --- backend/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/.env.example b/backend/.env.example index 453b8c8..e6f6fbd 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -6,4 +6,4 @@ TUNNEL_PORT=5433 # backend server env vars DATABASE_URL=postgresql://webapp:@localhost:5433/gamesdb PORT=3001 -#JWT_TOKEN= +#JWT_SECRET= From b257dafe702995e2ecfefe7163841e2fa8a61f4d Mon Sep 17 00:00:00 2001 From: Clemens Beese Date: Thu, 22 Oct 2026 14:44:27 +0200 Subject: [PATCH 2/9] refactor(frontend): eksporter GameNode --- frontend/src/graphql/games.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/graphql/games.ts b/frontend/src/graphql/games.ts index cdf81fc..c50d813 100644 --- a/frontend/src/graphql/games.ts +++ b/frontend/src/graphql/games.ts @@ -1,7 +1,7 @@ import { gql, type TypedDocumentNode } from '@apollo/client'; /** Ett spill slik backend sender det til resultatlista */ -interface GameNode { +export interface GameNode { id: string; name: string; released: string | null; From b7ce1cb92ef3d6241da86387fd3b4ad3e06e7457 Mon Sep 17 00:00:00 2001 From: Clemens Beese Date: Thu, 22 Oct 2026 14:44:27 +0200 Subject: [PATCH 3/9] =?UTF-8?q?feat(frontend):=20sp=C3=B8rring=20for=20rad?= =?UTF-8?q?ene=20p=C3=A5=20forsiden=20(aliaser=20og=20fragment)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/graphql/homeRows.ts | 71 ++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 frontend/src/graphql/homeRows.ts diff --git a/frontend/src/graphql/homeRows.ts b/frontend/src/graphql/homeRows.ts new file mode 100644 index 0000000..b8105c5 --- /dev/null +++ b/frontend/src/graphql/homeRows.ts @@ -0,0 +1,71 @@ +import { gql, type TypedDocumentNode } from '@apollo/client'; +import type { GameNode } from './games'; + +interface Row { + edges: { node: GameNode }[]; +} + +/** Svaret: en liste med spill per rad, med navnene fra aliasene i spørringen */ +export interface HomeRowsData { + popular: Row; + pc: Row; + playstation: Row; + nintendo: Row; +} + +/** Plattform-ID-ene for hver rad (se PLATFORMS i Home.tsx) */ +interface HomeRowsVars { + pc: string[]; + playstation: string[]; + nintendo: string[]; +} + +/** + * Henter alle radene på forsiden i en forespørsel. Hver rad er games-queryen med et + * eget navn (alias) og eget filter. Bare 12 spill per rad og ingen totalCount, + * så databasen slipper å telle alle treff. + */ +export const HOME_ROWS: TypedDocumentNode = gql` + fragment RowGame on Game { + id + name + released + backgroundImage + rating + metacritic + genres { + name + } + } + + query HomeRows($pc: [ID!], $playstation: [ID!], $nintendo: [ID!]) { + popular: games(first: 12) { + edges { + node { + ...RowGame + } + } + } + pc: games(filter: { platformIds: $pc }, first: 36) { + edges { + node { + ...RowGame + } + } + } + playstation: games(filter: { platformIds: $playstation }, first: 36) { + edges { + node { + ...RowGame + } + } + } + nintendo: games(filter: { platformIds: $nintendo }, first: 36) { + edges { + node { + ...RowGame + } + } + } + } +`; From 8ce887e899963e06e621c7b3395e4ee25309f2c5 Mon Sep 17 00:00:00 2001 From: Clemens Beese Date: Thu, 22 Oct 2026 14:44:27 +0200 Subject: [PATCH 4/9] style(frontend): vannrett rad med spillkort --- frontend/src/components/GameRow/GameRow.css | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 frontend/src/components/GameRow/GameRow.css diff --git a/frontend/src/components/GameRow/GameRow.css b/frontend/src/components/GameRow/GameRow.css new file mode 100644 index 0000000..dd8d63b --- /dev/null +++ b/frontend/src/components/GameRow/GameRow.css @@ -0,0 +1,40 @@ +.game-row { + margin-bottom: 40px; +} + +/* Overskrift til venstre, "se alle" til høyre, tynn linje under */ +.game-row__header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border); +} + +.game-row__all { + font-family: var(--mono); + font-size: 14px; + text-decoration: none; +} + +/* Kortene ligger bortover og scrolles vannrett */ +.game-row__list { + display: grid; + grid-auto-flow: column; /* nye kort legges til høyre, ikke under */ + grid-auto-columns: 220px; /* fast bredde per kort */ + gap: 20px; + margin: 0; + padding: 0 0 12px; /* plass til scrollbaren */ + list-style: none; + overflow-x: auto; + scroll-snap-type: x mandatory; /* kortene stopper pent på kanten når man sveiper */ + scrollbar-width: thin; +} + +/* Kortet fyller hele høyden, så bunnlinjene står likt */ +.game-row__list > li { + display: flex; + flex-direction: column; + scroll-snap-align: start; +} From ce080a49e0637745ac9e04ca047c04be70670895 Mon Sep 17 00:00:00 2001 From: Clemens Beese Date: Thu, 22 Oct 2026 14:44:27 +0200 Subject: [PATCH 5/9] feat(frontend): GameRow med overskrift, se alle og spillkort --- frontend/src/components/GameRow/GameRow.tsx | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 frontend/src/components/GameRow/GameRow.tsx diff --git a/frontend/src/components/GameRow/GameRow.tsx b/frontend/src/components/GameRow/GameRow.tsx new file mode 100644 index 0000000..3c6848d --- /dev/null +++ b/frontend/src/components/GameRow/GameRow.tsx @@ -0,0 +1,55 @@ +import { useId } from 'react'; +import { Link } from 'react-router'; +import type { GameSummary } from '../../types/game'; +import { GameCard } from '../GameCard/GameCard'; +import './GameRow.css'; + +interface GameRowProps { + title: string; + games: GameSummary[]; + favorites: Set; + onToggleFavorite: (id: string) => void; + /** Kalles før "se alle" går til listesiden, så App kan sette riktig filter */ + onShowAll: () => void; +} + +/** + * En rad på forsiden: overskrift, "se alle" og spillkort som scrolles bortover. + * Ingen automatisk rotering (WCAG). Kortene kan nås med Tab, og nettleseren + * scroller raden av seg selv til kortet som har fokus. + */ +export function GameRow({ title, games, favorites, onToggleFavorite, onShowAll }: GameRowProps) { + const headingId = useId(); // kobler
til overskriften for skjermlesere + + return ( +
+
+

+ {title} +

+ {/* aria-label så skjermlesere hører hvilken rad "se alle" gjelder */} + + se alle → + +
+ +
    + {games.map((game) => ( +
  • + +
  • + ))} +
+
+ ); +} From 62d88a3194c909e80142e3521dedaa7e5cccc5c6 Mon Sep 17 00:00:00 2001 From: Clemens Beese Date: Thu, 22 Oct 2026 14:44:27 +0200 Subject: [PATCH 6/9] =?UTF-8?q?feat(frontend):=20forside=20med=20popul?= =?UTF-8?q?=C3=A6re=20spill=20totalt=20og=20per=20plattform,=20uten=20dubl?= =?UTF-8?q?etter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Home/Home.tsx | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 frontend/src/pages/Home/Home.tsx diff --git a/frontend/src/pages/Home/Home.tsx b/frontend/src/pages/Home/Home.tsx new file mode 100644 index 0000000..af86dc8 --- /dev/null +++ b/frontend/src/pages/Home/Home.tsx @@ -0,0 +1,68 @@ +import { useQuery } from '@apollo/client/react'; +import { GameRow } from '../../components/GameRow/GameRow'; +import { HOME_ROWS, type HomeRowsData } from '../../graphql/homeRows'; + +// Plattform-ID-er fra databasen. Sjekk dem med { platforms { id name } } i Apollo Sandbox +const PLATFORMS = { + pc: ['9'], + playstation: ['10'], + nintendo: ['8'], +}; + +// Radene på forsiden, i rekkefølge. key er aliaset i spørringen +const ROWS: { key: keyof HomeRowsData; title: string; platformIds: string[] }[] = [ + // MIDLERTIDIG: bruker RAWG sin popularitet til brukernes egen rating er på plass + { key: 'popular', title: 'Populære nå', platformIds: [] }, + { key: 'pc', title: 'Populære på PC', platformIds: PLATFORMS.pc }, + { key: 'playstation', title: 'Populære på PlayStation', platformIds: PLATFORMS.playstation }, + { key: 'nintendo', title: 'Populære på Nintendo', platformIds: PLATFORMS.nintendo }, +]; + +// Antall spill som vises i hver rad +const ROW_SIZE = 12; + +interface HomeProps { + favorites: Set; + onToggleFavorite: (id: string) => void; + /** "Se alle" på en rad: App setter plattformfilteret før listesiden vises */ + onShowAll: (platformIds: string[]) => void; +} + +/** Forsiden: rader med populære spill, totalt og per plattform. */ +export function Home({ favorites, onToggleFavorite, onShowAll }: HomeProps) { + const { data, loading, error } = useQuery(HOME_ROWS, { variables: PLATFORMS }); + + // Hver rad viser bare spill som ikke allerede står i en rad over. + // De mest populære spillene finnes på nesten alle plattformer, så uten dette + // ville alle radene startet med de samme spillene. + const shown = new Set(); + const rows = ROWS.map((row) => { + const games = (data?.[row.key].edges ?? []) + .map(({ node }) => node) + .filter((game) => !shown.has(game.id)) // hopp over spill som er vist over + .slice(0, ROW_SIZE) // ta de 12 første av de som er igjen + // Backend gir sjangre som objekter ({ name }), spillkortet forventer bare navnene + .map((game) => ({ ...game, genres: game.genres.map((g) => g.name) })); + games.forEach((game) => shown.add(game.id)); // husk dem til neste rad + return { ...row, games }; + }); + + return ( +
+ {loading && !data &&

Laster …

} + {error &&

Kunne ikke hente spill: {error.message}

} + + {data && + rows.map((row) => ( + onShowAll(row.platformIds)} + /> + ))} +
+ ); +} From d614a4f82cbad506f50b5582ee82cc790ea4ae00 Mon Sep 17 00:00:00 2001 From: Clemens Beese Date: Thu, 22 Oct 2026 14:44:27 +0200 Subject: [PATCH 7/9] =?UTF-8?q?fix(frontend):=20tilbake=20fra=20detaljside?= =?UTF-8?q?n=20g=C3=A5r=20til=20listesiden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/GameDetail/GameDetail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/GameDetail/GameDetail.tsx b/frontend/src/pages/GameDetail/GameDetail.tsx index b53947c..7118154 100644 --- a/frontend/src/pages/GameDetail/GameDetail.tsx +++ b/frontend/src/pages/GameDetail/GameDetail.tsx @@ -55,7 +55,7 @@ export function GameDetail({ favorites, onToggleFavorite }: GameDetailProps) { }, [loadedId, title]); const backLink = ( - + ← tilbake til resultatene ); From cb6c56bbf3cc91c3a50c84d908dfa6c2fd5f0a62 Mon Sep 17 00:00:00 2001 From: Clemens Beese Date: Thu, 22 Oct 2026 14:44:27 +0200 Subject: [PATCH 8/9] =?UTF-8?q?feat(frontend):=20forside=20p=C3=A5=20/,=20?= =?UTF-8?q?listesiden=20p=C3=A5=20/spill,=20se=20alle=20setter=20plattform?= =?UTF-8?q?filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 54 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c56c5da..2670969 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import { GameDetail } from './pages/GameDetail/GameDetail'; import { GAMES, type GameSort } from './graphql/games'; import { Pagination } from './components/Pagination/Pagination'; import { SortSelect } from './components/SortSelect/SortSelect'; +import { Home } from './pages/Home/Home'; import type { Suggestion } from './graphql/searchSuggestions'; import type { GameSummary, GameView } from './types/game'; import './App.css'; @@ -31,7 +32,8 @@ function App() { const [view, setView] = useState('list'); const [showFavorites, setShowFavorites] = useState(false); const navigate = useNavigate(); - const onHome = useLocation().pathname === '/'; + // Søk, filtre og sortering vises på listesiden /spill. Forsiden / har rader + const onListPage = useLocation().pathname === '/spill'; // Søket i hovedlista. Settes først når brukeren trykker Enter, ikke for hver bokstav // (forslagene i søkefeltet tar seg av det), så lista og pagineringen ikke hopper @@ -68,24 +70,24 @@ function App() { // Valgene i dropdownene (sjangre og plattformer) fra databasen. // Filtrene vises bare på forsiden, så de hentes ikke på detaljsiden. - const { data: options } = useQuery(FILTER_OPTIONS, { skip: !onHome }); + const { data: options } = useQuery(FILTER_OPTIONS, { skip: !onListPage }); // Valgt forslag i søkefeltet åpner detaljsiden til spillet const handleSelectGame = (game: Suggestion) => navigate(`/game/${game.id}`); - // Søkeresultater og favoritter vises på forsiden, så fra detaljsiden går vi dit + // Søkeresultater vises på listesiden. Tømmes søket (X) andre steder, blir man der man er const handleSearch = (term: string) => { setQuery(term); - resetPage(); // nytt søk starter på side 1 - if (!onHome) navigate('/'); + resetPage(); + if (term && !onListPage) navigate('/spill'); }; const handleToggleFavorites = () => { - if (onHome) { + if (onListPage) { setShowFavorites((v) => !v); } else { setShowFavorites(true); - navigate('/'); + navigate('/spill'); } }; @@ -100,6 +102,18 @@ function App() { resetPage(); }; + // "Se alle" på en rad på forsiden: lista med plattformfilteret, uten søk, + // mest populære først. Selve sidebyttet gjør lenken i GameRow + const handleShowAll = (platformIds: string[]) => { + setQuery(''); + setFilters({ ...defaultFilters, platformIds }); + setSort('POPULARITY'); + setShowFavorites(false); + resetPage(); + setSearchResetKey((k) => k + 1); + window.scrollTo({ top: 0 }); + }; + // Forrige side: siste 20 før startCursor. Ellers: første 20 etter endCursor (eller fra start) const pageVars = cursor.before ? { last: PAGE_SIZE, before: cursor.before } @@ -114,7 +128,7 @@ function App() { sort, ...pageVars, }, - skip: !onHome, + skip: !onListPage, }); const result = data ?? previousData; // forrige liste vises mens den nye lastes @@ -152,11 +166,20 @@ function App() { // Når favoritter lagres i databasen, bør de hentes derfra i stedet. const games = showFavorites ? allGames.filter((g) => favorites.has(g.id)) : allGames; + // Navnene på valgte sjangre og plattformer, så det synes at lista er filtrert + // (f.eks. etter "se alle" på forsiden, når filtergruppene i sidefeltet er lukket) + const activeFilterNames = [ + ...(options?.genres ?? []).filter((g) => filters.genreIds.includes(g.id)), + ...(options?.platforms ?? []).filter((p) => filters.platformIds.includes(p.id)), + ].map((f) => f.name); + // Teksten over lista. role="status" gjør at skjermlesere leser den opp når den endres let status = ''; if (loading && !result) status = 'Laster …'; - else if (result && !showFavorites) + else if (result && !showFavorites) { status = `${result.games.totalCount.toLocaleString('nb-NO')} treff`; + if (activeFilterNames.length) status += ` · ${activeFilterNames.join(', ')}`; + } const toggleFavorite = (id: string) => { setFavorites((prev) => { @@ -175,13 +198,24 @@ function App() { onSearch={handleSearch} onSelectGame={handleSelectGame} favoritesCount={favorites.size} - showFavorites={showFavorites && onHome} + showFavorites={showFavorites && onListPage} onToggleFavorites={handleToggleFavorites} /> + {/* Forsiden med rader */} + } + /> + Date: Thu, 22 Oct 2026 14:44:27 +0200 Subject: [PATCH 9/9] docs: beskriv forsiden og listesiden i README --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index a2403d3..8aad54a 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,21 @@ Resultatlista kan sorteres på: **Tilgjengelighet:** Menyen er en vanlig `