From ab8409992060cfaf67faf0b9320b87f953c48b75 Mon Sep 17 00:00:00 2001 From: Jirachal Date: Thu, 15 Oct 2026 02:13:40 +0200 Subject: [PATCH 1/2] add catalogue pagination support --- server/src/graphQL/mockAlbums.ts | 108 +++++++++++++++++++++++++++++++ server/src/graphQL/resolvers.ts | 23 +++++-- server/src/graphQL/schema.ts | 5 +- 3 files changed, 130 insertions(+), 6 deletions(-) diff --git a/server/src/graphQL/mockAlbums.ts b/server/src/graphQL/mockAlbums.ts index b4b5d3d..a431153 100644 --- a/server/src/graphQL/mockAlbums.ts +++ b/server/src/graphQL/mockAlbums.ts @@ -50,4 +50,112 @@ export const MOCK_ALBUMS: Album[] = [ { title: 'Beat It', duration: 258000 }, ], }, + { + id: '4', + title: 'In Rainbows', + artist: 'Radiohead', + releaseYear: 2007, + genre: 'Rock', + coverUrl: null, + tracks: [ + { title: '15 Step', duration: 237000 }, + { title: 'Bodysnatchers', duration: 242000 }, + ], + }, + { + id: '5', + title: 'Number Ones', + artist: 'Michael Jackson', + releaseYear: 2002, + genre: 'Pop', + coverUrl: null, + tracks: [ + { title: 'Billie Jean', duration: 294000 }, + { title: 'Beat It', duration: 258000 }, + ], + }, + { + id: '6', + title: '9 to 5 and Odd Jobs', + artist: 'Dolly Parton', + releaseYear: 1980, + genre: 'Country', + coverUrl: null, + tracks: [ + { title: '9 to 5', duration: 163000 }, + { title: 'Hush-A-Bye Hard Times', duration: 210000 }, + ], + }, + { + id: '7', + title: 'Rumours', + artist: 'Fleetwood Mac', + releaseYear: 1977, + genre: 'Rock', + coverUrl: null, + tracks: [ + { title: 'Dreams', duration: 257000 }, + { title: 'The Chain', duration: 270000 }, + ], + }, + { + id: '8', + title: 'Back to Black', + artist: 'Amy Winehouse', + releaseYear: 2006, + genre: 'Soul', + coverUrl: null, + tracks: [ + { title: 'Rehab', duration: 214000 }, + { title: 'Back to Black', duration: 241000 }, + ], + }, + { + id: '9', + title: 'Discovery', + artist: 'Daft Punk', + releaseYear: 2001, + genre: 'Electronic', + coverUrl: null, + tracks: [ + { title: 'One More Time', duration: 320000 }, + { title: 'Digital Love', duration: 298000 }, + ], + }, + { + id: '10', + title: 'AM', + artist: 'Arctic Monkeys', + releaseYear: 2013, + genre: 'Rock', + coverUrl: null, + tracks: [ + { title: 'Do I Wanna Know?', duration: 272000 }, + { title: 'R U Mine?', duration: 201000 }, + ], + }, + { + id: '11', + title: '2', + artist: 'Mac DeMarco', + releaseYear: 2012, + genre: 'Pop', + coverUrl: null, + tracks: [ + { title: 'Dreaming', duration: 147000 }, + { title: 'Freaking Out the Neighborhood', duration: 173000 }, + ], + }, + { + id: '12', + title: 'Doggystyle', + artist: 'Snoop Dogg', + releaseYear: 1993, + genre: 'HipHop', + coverUrl: null, + tracks: [ + { title: 'Bathtub', duration: 110000 }, + { title: 'G Funk Intro', duration: 144000 }, + ], + }, ]; \ No newline at end of file diff --git a/server/src/graphQL/resolvers.ts b/server/src/graphQL/resolvers.ts index 3d26963..4dff658 100644 --- a/server/src/graphQL/resolvers.ts +++ b/server/src/graphQL/resolvers.ts @@ -1,8 +1,9 @@ +import { start } from 'node:repl'; import { MOCK_ALBUMS } from './mockAlbums.js'; export const resolvers = { Query: { - albums: (_: unknown, args: { search?: string; genre?: string; decade?: number; first?: number }) => { + albums: (_: unknown, args: { search?: string; genre?: string; decade?: number; first?: number, after?: string }) => { let filtered = [...MOCK_ALBUMS]; // Søkefilter på tittel eller artist @@ -30,14 +31,28 @@ export const resolvers = { ); } - const limit = args.first || 10; - const sliced = filtered.slice(0, limit); + const limit = args.first ?? 8; + + let startIndex = 0; + + if (args.after) { + const cursorIndex = filtered.findIndex( + (album) => album.id === args.after + ); + + if (cursorIndex !== -1) { + startIndex = cursorIndex + 1; + } + } + + const sliced = filtered.slice(startIndex, startIndex + limit); return { edges: sliced, + totalCount: filtered.length, pageInfo: { endCursor: sliced.length > 0 ? sliced[sliced.length - 1].id : null, - hasNextPage: filtered.length > limit, + hasNextPage: startIndex + sliced.length < filtered.length, }, }; }, diff --git a/server/src/graphQL/schema.ts b/server/src/graphQL/schema.ts index 70734f9..ebbd282 100644 --- a/server/src/graphQL/schema.ts +++ b/server/src/graphQL/schema.ts @@ -11,7 +11,7 @@ export const typeDefs = `#graphql releaseYear: Int! genre: String! coverUrl: String - tracks: [Track!] + tracks: [Track!]! } type PageInfo { @@ -22,6 +22,7 @@ export const typeDefs = `#graphql type AlbumConnection { edges: [Album!]! pageInfo: PageInfo! + totalCount: Int! } type Query { @@ -30,7 +31,7 @@ export const typeDefs = `#graphql genre: String decade: Int sortBy: String - first: Int = 10 + first: Int = 8 after: String ): AlbumConnection! From c8aaaeeeb6c5d2c313dfc8a8e0587334f148c5c5 Mon Sep 17 00:00:00 2001 From: Jirachal Date: Thu, 15 Oct 2026 12:08:37 +0200 Subject: [PATCH 2/2] connect catalogue page to graphql backend (Closes #51) --- src/components/AlbumCard/AlbumCard.tsx | 2 +- src/graphQL/queries.ts | 34 +++++- src/pages/CataloguePage.tsx | 139 ++++++++++++++++--------- src/types/album.ts | 18 ++-- 4 files changed, 134 insertions(+), 59 deletions(-) diff --git a/src/components/AlbumCard/AlbumCard.tsx b/src/components/AlbumCard/AlbumCard.tsx index e6a1673..07e8ad2 100644 --- a/src/components/AlbumCard/AlbumCard.tsx +++ b/src/components/AlbumCard/AlbumCard.tsx @@ -28,7 +28,7 @@ function AlbumCard({ album }: AlbumCardProps) {

{album.artist}

{album.releaseYear} - {album.genre.join(' / ')} + {album.genre}
diff --git a/src/graphQL/queries.ts b/src/graphQL/queries.ts index d6a8d35..682bfb0 100644 --- a/src/graphQL/queries.ts +++ b/src/graphQL/queries.ts @@ -1,7 +1,5 @@ import { gql } from '@apollo/client' -// Temporary types matching the current mock graphql schema -// Update when the backend switches to the PostgreSQL-backend typeDefs export const GET_ALBUM = gql` query GetAlbum($id: ID!) { album(id: $id) { @@ -18,4 +16,36 @@ export const GET_ALBUM = gql` } } } +` + +export const GET_ALBUMS = gql` + query GetAlbums( + $search: String + $genre: String + $decade: Int + $first: Int + $after: String + ) { + albums( + search: $search + genre: $genre + decade: $decade + first: $first + after: $after + ) { + edges { + id + title + artist + releaseYear + genre + coverUrl + } + totalCount + pageInfo { + endCursor + hasNextPage + } + } + } ` \ No newline at end of file diff --git a/src/pages/CataloguePage.tsx b/src/pages/CataloguePage.tsx index 1baa16a..bec3dbb 100644 --- a/src/pages/CataloguePage.tsx +++ b/src/pages/CataloguePage.tsx @@ -1,23 +1,59 @@ -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useState } from 'react' import '../App.css' -import albumData from '../mock/albums.json' import type { Album } from '../types/album' import AlbumList from '../components/AlbumList/AlbumList' import SearchBar from '../components/SearchBar/SearchBar' import FilterControls from '../components/FilterControls/FilterControls' import Header from '../components/Header/Header' +import { useQuery } from '@apollo/client/react' +import { GET_ALBUMS } from '../graphQL/queries' -function CataloguePage() { - const albums: Album[] = albumData as Album[] +type GetAlbumsData = { + albums: { + edges: Album[] + totalCount: number + pageInfo: { + endCursor: string | null + hasNextPage: boolean + } + } +} + +type GetAlbumsVariables = { + search?: string + genre?: string + decade?: number + first?: number + after?: string | null +} +function CataloguePage() { const [search, setSearch] = useState('') + const [debouncedSearch, setDebouncedSearch] = useState('') + + useEffect(() => { + const timeout = setTimeout(() => { + setDebouncedSearch(search) + }, 400) + + return () => clearTimeout(timeout) + }, [search]) + const [genre, setGenre] = useState('All genres') const [decade, setDecade] = useState('All decades') + const [rating, setRating] = useState(null) + const [filtersVisible, setFiltersVisible] = useState(true) + const [currentPage, setCurrentPage] = useState(1) + const [after, setAfter] = useState(null) + const [cursorHistory, setCursorHistory] = useState<(string | null)[]>([]) + + const selectedGenre = genre === 'All genres' ? undefined : genre + const selectedDecade = decade === 'All decades' ? undefined : parseInt(decade) const genres = [ 'All genres', @@ -51,43 +87,31 @@ function CataloguePage() { '2020s', ] - const filteredAlbums = useMemo(() => { - return albums.filter((album) => { - const matchesSearch = - `${album.title} ${album.artist}` - .toLowerCase() - .includes(search.toLowerCase()) - - const matchesGenre = - genre === 'All genres' || album.genre.includes(genre) - - const albumDecade = `${Math.floor(album.releaseYear / 10) * 10}s` - const matchesDecade = - decade === 'All decades' || albumDecade === decade - - // Rating er kun en visuell kontroll inntil rating-data finnes. - const matchesRating = rating === null - - return ( - matchesSearch && - matchesGenre && - matchesDecade && - matchesRating - ) - }) - }, [albums, search, genre, decade, rating]) - const pageSize = filtersVisible ? 6 : 8 - const totalPages = Math.ceil(filteredAlbums.length / pageSize) - const visibleAlbums = filteredAlbums.slice( - (currentPage - 1) * pageSize, - currentPage * pageSize, - ) + const { data, loading, error } = useQuery< + GetAlbumsData, + GetAlbumsVariables + >(GET_ALBUMS, { + variables: { + search: debouncedSearch || undefined, + genre: selectedGenre, + decade: selectedDecade, + first: pageSize, + after, + }, + }) useEffect(() => { setCurrentPage(1) - }, [search, genre, decade, filtersVisible]) + setAfter(null) + setCursorHistory([]) + }, [debouncedSearch, genre, decade, filtersVisible]) + + const albums = data?.albums.edges ?? [] + const totalCount = data?.albums.totalCount ?? 0 + + const totalPages = Math.ceil(totalCount / pageSize) function clearFilters() { setSearch('') @@ -96,6 +120,26 @@ function CataloguePage() { setRating(null) } + function nextPage() { + const endCursor = data?.albums.pageInfo.endCursor + + if (!endCursor) return + + setCursorHistory((history) => [...history, after]) + setAfter(endCursor) + setCurrentPage((page) => page + 1) + } + + function previousPage() { + if (currentPage <= 1) return + + const previousAfter = cursorHistory[cursorHistory.length - 1] ?? null + + setAfter(previousAfter) + setCursorHistory((history) => history.slice(0, -1)) + setCurrentPage((page) => page - 1) + } + return (
@@ -183,25 +227,28 @@ function CataloguePage() { >

- {filteredAlbums.length}{' '} - {filteredAlbums.length === 1 ? 'album' : 'albums'} found + {totalCount}{' '} + {totalCount === 1 ? 'album' : 'albums'} found

CURATED FOR YOU
- {filteredAlbums.length === 0 ? ( -
+ {loading ? ( +

Loading albums...

+ ) : error ? ( +

Error loading albums: {error.message}

+ ) : totalCount === 0 ? ( +

No albums found

Try another search or adjust your filters.

-
) : ( - + )}
diff --git a/src/types/album.ts b/src/types/album.ts index 8a2f1c9..2ae26c2 100644 --- a/src/types/album.ts +++ b/src/types/album.ts @@ -1,16 +1,16 @@ // src/types/album.ts export interface Track { - title: string; - duration: string; + title: string + duration: number } export interface Album { - id: string; - title: string; - artist: string; - releaseYear: number; - genre: string[]; - coverUrl: string; - tracks: Track[]; + id: string + title: string + artist: string + releaseYear: number + genre: string + coverUrl: string | null + tracks: Track[] } \ No newline at end of file