diff --git a/src/App.css b/src/App.css
index 3fa5508..2102435 100644
--- a/src/App.css
+++ b/src/App.css
@@ -6,60 +6,13 @@
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 {
+.catalogue-main,
+.album-detail-content {
max-width: 1600px;
margin: 0 auto;
padding: 44px 5% 60px;
@@ -320,5 +273,4 @@
.results-heading span {
display: none;
}
-
-}
\ No newline at end of file
+}
diff --git a/src/components/AlbumCard/AlbumCard.css b/src/components/AlbumCard/AlbumCard.css
index 4e3ab3d..b079d28 100644
--- a/src/components/AlbumCard/AlbumCard.css
+++ b/src/components/AlbumCard/AlbumCard.css
@@ -24,23 +24,13 @@
outline-offset: 4px;
}
-.album-image {
- display: block;
- width: 100%;
- aspect-ratio: 1 / 1;
- object-fit: cover;
- border: 1px solid #947268;
- background: #947268;
+.album-card:hover {
+ transform: translateY(-4px);
}
-.album-placeholder {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: flex-start;
- padding: 18px;
- background: #74070E;
- color: #F4E3B2;
+.album-card:focus-visible {
+ outline: 2px solid #74070E;
+ outline-offset: 4px;
}
.album-card:nth-child(2n) .album-placeholder {
@@ -52,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 560440c..e6a1673 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,12 @@ function AlbumCard({ album }: AlbumCardProps) {
className="album-card"
key={album.id}
>
- {album.coverUrl ? (
-
- ) : (
-
{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..ef0e84a
--- /dev/null
+++ b/src/components/AlbumCover/AlbumCover.tsx
@@ -0,0 +1,44 @@
+import { useEffect, useState } from "react"
+import './AlbumCover.css'
+
+type AlbumCoverProps = {
+ coverUrl: string | null
+ title: string
+ artist: string
+ className?: string
+}
+
+function AlbumCover({ coverUrl, title, artist, className=''}: AlbumCoverProps) {
+ const [imageError, setImageError] = useState(false)
+
+ useEffect(() => {
+ setImageError(false)
+ }, [coverUrl])
+
+ if (coverUrl && !imageError) {
+ return (
+

setImageError(true)}
+ />
+ )
+ }
+ return (
+ //Previous placholder implementation kept temporarily
+ /*
+
+ 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/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.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..a24f989 100644
--- a/src/pages/AlbumDetailPage.tsx
+++ b/src/pages/AlbumDetailPage.tsx
@@ -1,9 +1,125 @@
import { useParams } from "react-router-dom"
+import './AlbumDetailPage.css'
+
+import Header from '../components/Header/Header'
+import AlbumCover from '../components/AlbumCover/AlbumCover'
+
+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 { 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 ID: {id}
+
+
+
+
+
+
+
+
+
{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.
+
+
+
+
+
+
)
}
diff --git a/src/pages/CataloguePage.tsx b/src/pages/CataloguePage.tsx
index 000ead0..1baa16a 100644
--- a/src/pages/CataloguePage.tsx
+++ b/src/pages/CataloguePage.tsx
@@ -1,6 +1,5 @@
-
import { useEffect, 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[]
@@ -98,23 +98,7 @@ function CataloguePage() {
return (
-
-
- CDLLECTION.
-
-
-
-
+