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 - ) : ( -
- 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..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 ( + {`Album 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. - - - -
+