From a7525fcb8fa30e068019b57fa4d3a83dee6fa768 Mon Sep 17 00:00:00 2001 From: Jirachal Date: Mon, 12 Oct 2026 23:04:08 +0200 Subject: [PATCH 1/5] extract reusable AlbomCover and Header components --- src/App.css | 48 ----------------------- src/components/AlbumCard/AlbumCard.css | 33 ---------------- src/components/AlbumCard/AlbumCard.tsx | 19 ++++----- src/components/AlbumCover/AlbumCover.css | 32 +++++++++++++++ src/components/AlbumCover/AlbumCover.tsx | 27 +++++++++++++ src/components/Header/Header.css | 50 ++++++++++++++++++++++++ src/components/Header/Header.tsx | 23 +++++++++++ src/pages/CataloguePage.tsx | 20 +--------- 8 files changed, 141 insertions(+), 111 deletions(-) create mode 100644 src/components/AlbumCover/AlbumCover.css create mode 100644 src/components/AlbumCover/AlbumCover.tsx create mode 100644 src/components/Header/Header.css create mode 100644 src/components/Header/Header.tsx diff --git a/src/App.css b/src/App.css index 72417f0..abd9702 100644 --- a/src/App.css +++ b/src/App.css @@ -6,58 +6,10 @@ color: #310E10; } -/* Header */ -.site-header { - min-height: 88px; - padding: 20px 5%; - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; - border-bottom: 1px solid #947268; -} - -.logo { - color: #74070E; - font-family: 'Clash Display', sans-serif; - font-size: clamp(1.8rem, 3vw, 2.5rem); - font-weight: 600; - letter-spacing: -1.5px; - text-decoration: none; -} - -.logo span, .catalogue-heading h1 span { color: #74070E; } -.header-actions { - display: flex; - align-items: center; - gap: 12px; -} - -.header-button { - padding: 10px 18px; - border: 1px solid #74070E; - border-radius: 3px; - background: transparent; - color: #74070E; - font-size: 0.9rem; - cursor: pointer; -} - -.sign-in-button { - background: #74070E; - color: #F4E3B2; -} - -.header-button:hover { - background: #310E10; - border-color: #310E10; - color: #F4E3B2; -} - /* Main content */ .catalogue-main { max-width: 1600px; diff --git a/src/components/AlbumCard/AlbumCard.css b/src/components/AlbumCard/AlbumCard.css index 435a35b..b079d28 100644 --- a/src/components/AlbumCard/AlbumCard.css +++ b/src/components/AlbumCard/AlbumCard.css @@ -33,25 +33,6 @@ outline-offset: 4px; } -.album-image { - display: block; - width: 100%; - aspect-ratio: 1 / 1; - object-fit: cover; - border: 1px solid #947268; - background: #947268; -} - -.album-placeholder { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: flex-start; - padding: 18px; - background: #74070E; - color: #F4E3B2; -} - .album-card:nth-child(2n) .album-placeholder { background: #45462A; } @@ -61,20 +42,6 @@ color: #310E10; } -.placeholder-mark { - font-family: 'Clash Display', sans-serif; - font-size: 2.8rem; - font-weight: 600; -} - -.placeholder-title { - max-width: 100%; - font-family: 'Clash Display', sans-serif; - font-size: clamp(1.1rem, 1.7vw, 1.8rem); - font-weight: 600; - overflow-wrap: anywhere; -} - .album-card-info { padding: 12px 2px 0; } diff --git a/src/components/AlbumCard/AlbumCard.tsx b/src/components/AlbumCard/AlbumCard.tsx index 2ed7fac..a51da01 100644 --- a/src/components/AlbumCard/AlbumCard.tsx +++ b/src/components/AlbumCard/AlbumCard.tsx @@ -2,6 +2,9 @@ import { Link } from 'react-router-dom' import type { Album } from '../../types/album' import './AlbumCard.css' +import AlbumCover from '../AlbumCover/AlbumCover' +import '../AlbumCover/AlbumCover.css' + interface AlbumCardProps { album: Album } @@ -13,18 +16,10 @@ function AlbumCard({ album }: AlbumCardProps) { className="album-card" key={album.id} > - {album.coverUrl ? ( - {`${album.title} - ) : ( -
- C. - {album.title} -
- )} +

{album.title}

diff --git a/src/components/AlbumCover/AlbumCover.css b/src/components/AlbumCover/AlbumCover.css new file mode 100644 index 0000000..10e25c6 --- /dev/null +++ b/src/components/AlbumCover/AlbumCover.css @@ -0,0 +1,32 @@ +.album-image { + display: block; + width: 100%; + aspect-ratio: 1 / 1; + object-fit: cover; + border: 1px solid #947268; + background: #947268; +} + +.album-placeholder { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + padding: 18px; + background: #74070E; + color: #F4E3B2; +} + +.placeholder-mark { + font-family: 'Clash Display', sans-serif; + font-size: 2.8rem; + font-weight: 600; +} + +.placeholder-title { + max-width: 100%; + font-family: 'Clash Display', sans-serif; + font-size: clamp(1.1rem, 1.7vw, 1.8rem); + font-weight: 600; + overflow-wrap: anywhere; +} \ No newline at end of file diff --git a/src/components/AlbumCover/AlbumCover.tsx b/src/components/AlbumCover/AlbumCover.tsx new file mode 100644 index 0000000..55487fb --- /dev/null +++ b/src/components/AlbumCover/AlbumCover.tsx @@ -0,0 +1,27 @@ +import './AlbumCover.css' + +type AlbumCoverProps = { + coverUrl: string | null + title: string + className?: string +} + +function AlbumCover({ coverUrl, title, className=''}: AlbumCoverProps) { + if (coverUrl) { + return ( + {`${title} + ) + } + return ( +
+ C. + {title} +
+ ) +} + +export default AlbumCover \ No newline at end of file diff --git a/src/components/Header/Header.css b/src/components/Header/Header.css new file mode 100644 index 0000000..b470af2 --- /dev/null +++ b/src/components/Header/Header.css @@ -0,0 +1,50 @@ +/* Header */ +.site-header { + min-height: 88px; + padding: 20px 5%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + border-bottom: 1px solid #947268; +} + +.logo { + color: #74070E; + font-family: 'Clash Display', sans-serif; + font-size: clamp(1.8rem, 3vw, 2.5rem); + font-weight: 600; + letter-spacing: -1.5px; + text-decoration: none; +} + +.logo span { + color: #74070E; +} + +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.header-button { + padding: 10px 18px; + border: 1px solid #74070E; + border-radius: 3px; + background: transparent; + color: #74070E; + font-size: 0.9rem; + cursor: pointer; +} + +.sign-in-button { + background: #74070E; + color: #F4E3B2; +} + +.header-button:hover { + background: #310E10; + border-color: #310E10; + color: #F4E3B2; +} \ No newline at end of file diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 0000000..caf8a25 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,23 @@ +import { Link } from 'react-router-dom' +import './Header.css' + +function Header() { + return ( +
+ + CDLLECTION. + + + +
+ ) +} + +export default Header \ No newline at end of file diff --git a/src/pages/CataloguePage.tsx b/src/pages/CataloguePage.tsx index b9c4df4..e08a861 100644 --- a/src/pages/CataloguePage.tsx +++ b/src/pages/CataloguePage.tsx @@ -1,6 +1,5 @@ import { useMemo, useState } from 'react' -import { Link } from 'react-router-dom' import '../App.css' import albumData from '../mock/albums.json' import type { Album } from '../types/album' @@ -8,6 +7,7 @@ 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' function CataloguePage() { const albums: Album[] = albumData as Album[] @@ -92,23 +92,7 @@ function CataloguePage() { return (
-
- - CDLLECTION. - - - -
+
From 476c854be2769e89a5aa0a3e2db5f346287623ad Mon Sep 17 00:00:00 2001 From: Jirachal Date: Tue, 13 Oct 2026 11:17:17 +0200 Subject: [PATCH 2/5] build album detail page layout with mock data and basic styling --- src/App.css | 17 +++++- src/pages/AlbumDetailPage.css | 106 ++++++++++++++++++++++++++++++++++ src/pages/AlbumDetailPage.tsx | 75 +++++++++++++++++++++++- 3 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 src/pages/AlbumDetailPage.css diff --git a/src/App.css b/src/App.css index abd9702..8fb1b84 100644 --- a/src/App.css +++ b/src/App.css @@ -11,7 +11,8 @@ } /* Main content */ -.catalogue-main { +.catalogue-main, +.album-detail-content { max-width: 1600px; margin: 0 auto; padding: 44px 5% 60px; @@ -251,4 +252,18 @@ .results-heading span { display: none; } +} + +.toggle-filters { + padding: 10px 14px; + border: 1px solid #45462A; + border-radius: 3px; + background: transparent; + color: #310E10; + cursor: pointer; +} + +.toggle-filters:hover { + background: #45462A; + color: #F4E3B2; } \ No newline at end of file diff --git a/src/pages/AlbumDetailPage.css b/src/pages/AlbumDetailPage.css new file mode 100644 index 0000000..0c4fdfe --- /dev/null +++ b/src/pages/AlbumDetailPage.css @@ -0,0 +1,106 @@ + +/* Detail-specific */ +.album-detail-content { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 32px; +} + +.album-detail-main { + display: grid; + grid-template-columns: 320px 1fr; + gap: 48px; + align-items: start; +} + +.album-detail-cover { + width: 100%; +} + +.album-detail-info h1 { + margin-top: 0; + margin-bottom: 6px; + font-family: 'Clash Display', sans-serif; +} + +.album-detail-info h2 { + margin: 0 0 20px; + font-size: 1.2rem; + font-weight: 400; +} + +.album-detail-info p { + margin: 6px 0; +} + +.album-detail-actions { + display: flex; + gap: 12px; + margin-top: 32px; +} + +.reviews-section { + grid-column: 1; + min-height: 180px; + padding: 28px; + border: 1px solid #947268; + border-radius: 3px; + + display: flex; + flex-direction: column; +} + +.reviews-section button { + align-self: flex-end; + margin-top:auto; +} + +.track-section { + grid-column: 2; + grid-row: 1 / span 2; + padding: 20px; + border: 1px solid #947268; + border-radius: 3px; +} + + +.album-detail-actions button, +.reviews-section button { + padding: 10px 14px; + border: 1px solid #45462A; + border-radius: 3px; + background: transparent; + color: #310E10; + cursor: pointer; +} + +.album-detail-actions button:hover, +.reviews-section button:hover { + background: #45462A; + color: #F4E3B2; +} + + +.album-tracks { + margin: 0; + padding: 0; + list-style: none; +} + +.album-tracks li { + display: flex; + justify-content: space-between; + gap: 20px; + padding: 12px 0; + border-bottom: 1px solid #947268; +} + +.track-duration { + flex-shrink: 0; +} + +.track-section h3, +.reviews-section h3 { + margin-top: 0; + font-family: 'Clash Display', sans-serif; +} \ No newline at end of file diff --git a/src/pages/AlbumDetailPage.tsx b/src/pages/AlbumDetailPage.tsx index 67d6dcd..8b62c4e 100644 --- a/src/pages/AlbumDetailPage.tsx +++ b/src/pages/AlbumDetailPage.tsx @@ -1,10 +1,79 @@ import { useParams } from "react-router-dom" +import './AlbumDetailPage.css' + +import Header from '../components/Header/Header' +import AlbumCover from '../components/AlbumCover/AlbumCover' +import albumData from '../mock/albums.json' function AlbumDetailPage() { const { id } = useParams() - return ( -

Album ID: {id}

- ) + const album = albumData.find((album) => album.id == id) + + if (!album) { + return ( +

Album not found

+ ) + } + return ( +
+
+ +
+
+ + +
+

{album.title}

+

{album.artist}

+ +

{album.releaseYear}

+

{album.genre.join(' / ')}

+

{album.tracks.length} tracks

+ +
+ + +
+
+ +
+ +
+

Tracks

+ +
    + {album.tracks.map((track, index) => ( +
  • + + {index + 1}. {track.title} + + + + {track.duration} + +
  • + ))} +
+
+ +
+

Reviews

+ +

+ No reviews yet. +

+ + +
+
+
+ ) } export default AlbumDetailPage \ No newline at end of file From 093063a68049ffbdec146318657722bb6f15f6f6 Mon Sep 17 00:00:00 2001 From: Jirachal Date: Tue, 13 Oct 2026 15:04:05 +0200 Subject: [PATCH 3/5] connect album detail page to graphql backend --- src/graphQL/queries.ts | 21 ++++ src/pages/AlbumDetailPage.tsx | 178 +++++++++++++++++++++------------- 2 files changed, 133 insertions(+), 66 deletions(-) create mode 100644 src/graphQL/queries.ts diff --git a/src/graphQL/queries.ts b/src/graphQL/queries.ts new file mode 100644 index 0000000..d6a8d35 --- /dev/null +++ b/src/graphQL/queries.ts @@ -0,0 +1,21 @@ +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) { + id + title + artist + releaseYear + genre + coverUrl + + tracks { + title + duration + } + } + } +` \ No newline at end of file diff --git a/src/pages/AlbumDetailPage.tsx b/src/pages/AlbumDetailPage.tsx index 8b62c4e..856503a 100644 --- a/src/pages/AlbumDetailPage.tsx +++ b/src/pages/AlbumDetailPage.tsx @@ -3,77 +3,123 @@ import './AlbumDetailPage.css' import Header from '../components/Header/Header' import AlbumCover from '../components/AlbumCover/AlbumCover' -import albumData from '../mock/albums.json' + +import { useQuery } from "@apollo/client/react" +import { GET_ALBUM } from "../graphQL/queries" + +// Temporary types matching the current mock graphql schema +// Update when the backend switches to the PostgreSQL-backend typeDefs +type GetAlbumData = { + album: { + id: string + title: string + artist: string + releaseYear: number + genre: string + coverUrl: string | null + tracks: { + title: string + duration: number | null + }[] + } | null +} + +type GetALbumVariables = { + id: string +} + +function formatDuration(durationMs: number | null) { + if (durationMs == null) return '' + + const totalSeconds = Math.floor(durationMs / 1000) + const minutes = Math.floor(totalSeconds / 60) + const seconds = totalSeconds % 60 + + return `${minutes}:${seconds.toString().padStart(2, '0')}` +} function AlbumDetailPage() { const { id } = useParams() - const album = albumData.find((album) => album.id == id) - - if (!album) { - return ( -

Album not found

- ) - } - return ( -
-
- -
-
- - -
-

{album.title}

-

{album.artist}

- -

{album.releaseYear}

-

{album.genre.join(' / ')}

-

{album.tracks.length} tracks

- -
- - -
+ const { data, loading, error } = useQuery< + GetAlbumData, + GetALbumVariables + >(GET_ALBUM, { + variables: { id: id! }, + skip: !id, + }) + + if (loading) return

Loading...

+ if (error) return

Error loading album detail: {error.message}

+ if (!data?.album) return

Album not found

+ + const album = data.album + + const totalDurationMs = album.tracks.reduce( + (total, track) => total + (track.duration ?? 0), + 0 + ) + const totalMinutes = Math.floor(totalDurationMs / 60000) + + return ( +
+
+ +
+
+ + +
+

{album.title}

+

{album.artist}

+ +

{album.releaseYear}

+

{album.genre}

+

{album.tracks.length} tracks • {totalMinutes} min

+ +
+ +
+
+ +
+ +
+

Tracks

+ +
    + {album.tracks.map((track, index) => ( +
  • + + {index + 1}. {track.title} + + + + {formatDuration(track.duration)} + +
  • + ))} +
+
+ +
+

Reviews

+ +

+ No reviews yet. +

-
- -
-

Tracks

- -
    - {album.tracks.map((track, index) => ( -
  • - - {index + 1}. {track.title} - - - - {track.duration} - -
  • - ))} -
-
- -
-

Reviews

- -

- No reviews yet. -

- - -
-
-
- ) + +
+
+
+ ) } export default AlbumDetailPage \ No newline at end of file From c2d60e3bc4204eb23c89cdc58a5487dd85887870 Mon Sep 17 00:00:00 2001 From: Jirachal Date: Tue, 13 Oct 2026 15:45:53 +0200 Subject: [PATCH 4/5] add album cover fallback placeholder --- src/components/AlbumCover/AlbumCover.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/AlbumCover/AlbumCover.tsx b/src/components/AlbumCover/AlbumCover.tsx index 55487fb..22cb39b 100644 --- a/src/components/AlbumCover/AlbumCover.tsx +++ b/src/components/AlbumCover/AlbumCover.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react" import './AlbumCover.css' type AlbumCoverProps = { @@ -7,20 +8,34 @@ type AlbumCoverProps = { } function AlbumCover({ coverUrl, title, className=''}: AlbumCoverProps) { - if (coverUrl) { + const [imageError, setImageError] = useState(false) + + useEffect(() => { + setImageError(false) + }, [coverUrl]) + + if (coverUrl && !imageError) { return ( {`${title} setImageError(true)} /> ) } return ( + /*
C. {title}
+ */ + ) } From bf28c736e044146dbcb145e06e6bd54c7b67af95 Mon Sep 17 00:00:00 2001 From: Jirachal Date: Tue, 13 Oct 2026 17:03:43 +0200 Subject: [PATCH 5/5] correct AlbumCover title and artist references --- src/components/AlbumCover/AlbumCover.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/AlbumCover/AlbumCover.tsx b/src/components/AlbumCover/AlbumCover.tsx index 6fe514c..ef0e84a 100644 --- a/src/components/AlbumCover/AlbumCover.tsx +++ b/src/components/AlbumCover/AlbumCover.tsx @@ -20,12 +20,13 @@ function AlbumCover({ coverUrl, title, artist, className=''}: AlbumCoverProps) { {`Album setImageError(true)} /> ) } return ( + //Previous placholder implementation kept temporarily /*
C.