diff --git a/src/App.css b/src/App.css index aae77cd..72417f0 100644 --- a/src/App.css +++ b/src/App.css @@ -209,7 +209,7 @@ background: #310E10; } -/* Album grid */ +/* Album section and result count */ .album-section { min-width: 0; } @@ -235,107 +235,7 @@ text-align: right; } -.album-displayer { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 24px 18px; -} - -.filters-hidden .album-displayer { - grid-template-columns: repeat(4, minmax(0, 1fr)); -} - -.album-card { - min-width: 0; - overflow: hidden; - color: #310E10; - text-decoration: none; - transition: transform 0.2s ease; -} - -.album-card:hover { - transform: translateY(-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; -} - -.album-card:nth-child(3n) .album-placeholder { - background: #947268; - 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; -} - -.album-card-info h2 { - margin: 0 0 5px; - font-family: 'Clash Display', sans-serif; - font-size: 1.15rem; - line-height: 1.2; -} - -.album-artist { - margin: 0 0 10px; - color: #45462A; - font-size: 0.9rem; -} - -.album-meta { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - gap: 6px; - color: #45462A; - font-size: 0.75rem; -} - -/* Empty state and pagination */ -.empty-state { - padding: 48px 20px; - border: 1px dashed #947268; - text-align: center; -} - -.empty-state h2 { - font-family: 'Clash Display', sans-serif; -} - +/* Pagination */ .pagination { display: flex; justify-content: center; @@ -350,6 +250,7 @@ border-radius: 2px; background: transparent; color: #74070E; + cursor: pointer; } .pagination button:disabled { @@ -367,11 +268,6 @@ grid-template-columns: 200px minmax(0, 1fr); gap: 20px; } - - .album-displayer, - .filters-hidden .album-displayer { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } } @media (max-width: 650px) { @@ -400,12 +296,6 @@ grid-template-columns: minmax(0, 1fr); } - .album-displayer, - .filters-hidden .album-displayer { - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 20px 12px; - } - .results-heading span { display: none; } diff --git a/src/components/AlbumCard/AlbumCard.css b/src/components/AlbumCard/AlbumCard.css new file mode 100644 index 0000000..435a35b --- /dev/null +++ b/src/components/AlbumCard/AlbumCard.css @@ -0,0 +1,102 @@ + +/* Individual album card */ + +.album-card { + display: block; + min-width: 0; + overflow: hidden; + border: 1px solid #947268; + border-radius: 3px; + padding: 12px; + background: #fff4d5; + color: #310E10; + text-decoration: none; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.album-card:hover { + transform: translateY(-4px); + box-shadow: 0 4px 12px rgb(49 14 16 / 15%); +} + +.album-card:focus-visible { + outline: 2px solid #74070E; + outline-offset: 4px; +} + +.album-card:hover { + transform: translateY(-4px); +} + +.album-card:focus-visible { + outline: 2px solid #74070E; + 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; +} + +.album-card:nth-child(3n) .album-placeholder { + background: #947268; + 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; +} + +.album-card-info h2 { + margin: 0 0 5px; + font-family: 'Clash Display', sans-serif; + font-size: 1.15rem; + line-height: 1.2; +} + +.album-artist { + margin: 0 0 10px; + color: #45462A; + font-size: 0.9rem; +} + +.album-meta { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 6px; + color: #45462A; + font-size: 0.75rem; +} \ No newline at end of file diff --git a/src/components/AlbumCard/AlbumCard.tsx b/src/components/AlbumCard/AlbumCard.tsx new file mode 100644 index 0000000..2ed7fac --- /dev/null +++ b/src/components/AlbumCard/AlbumCard.tsx @@ -0,0 +1,41 @@ +import { Link } from 'react-router-dom' +import type { Album } from '../../types/album' +import './AlbumCard.css' + +interface AlbumCardProps { + album: Album +} + +function AlbumCard({ album }: AlbumCardProps) { + return ( + + {album.coverUrl ? ( + {`${album.title} + ) : ( +
+ C. + {album.title} +
+ )} + +
+

{album.title}

+

{album.artist}

+
+ {album.releaseYear} + {album.genre.join(' / ')} +
+
+ + ) +} + +export default AlbumCard \ No newline at end of file diff --git a/src/components/AlbumList/AlbumList.css b/src/components/AlbumList/AlbumList.css new file mode 100644 index 0000000..3ee2e27 --- /dev/null +++ b/src/components/AlbumList/AlbumList.css @@ -0,0 +1,39 @@ + +/* Album grid */ +.album-displayer { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 24px 18px; +} + +/* More space when filters are hidden */ +.filters-hidden .album-displayer { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +/* Empty search results */ +.empty-state { + padding: 48px 20px; + border: 1px dashed #947268; + text-align: center; +} + +.empty-state h2 { + font-family: 'Clash Display', sans-serif; +} + +/* Smaller screens */ +@media (max-width: 1050px) { + .album-displayer, + .filters-hidden .album-displayer { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 650px) { + .album-displayer, + .filters-hidden .album-displayer { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px 12px; + } +} \ No newline at end of file diff --git a/src/components/AlbumList/AlbumList.tsx b/src/components/AlbumList/AlbumList.tsx new file mode 100644 index 0000000..400e3c2 --- /dev/null +++ b/src/components/AlbumList/AlbumList.tsx @@ -0,0 +1,29 @@ + +import type { Album } from '../../types/album' +import AlbumCard from '../AlbumCard/AlbumCard' +import './AlbumList.css' + +interface AlbumListProps { + albums: Album[] +} + +function AlbumList({ albums }: AlbumListProps) { + if (albums.length === 0) { + return ( +
+

No albums found

+

Try another search or clear your filters.

+
+ ) + } + + return ( +
+ {albums.map((album) => ( + + ))} +
+ ) +} + +export default AlbumList \ No newline at end of file diff --git a/src/components/FilterControls/FilterControls.tsx b/src/components/FilterControls/FilterControls.tsx new file mode 100644 index 0000000..bcff5fd --- /dev/null +++ b/src/components/FilterControls/FilterControls.tsx @@ -0,0 +1,51 @@ + + +interface FilterControlsProps { + genres: string[] + decades: string[] + genre: string + decade: string + onGenreChange: (value: string) => void + onDecadeChange: (value: string) => void +} + +function FilterControls({ + genres, + decades, + genre, + decade, + onGenreChange, + onDecadeChange, +}: FilterControlsProps) { + return ( + <> + + + + + + + ) +} + +export default FilterControls \ No newline at end of file diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx new file mode 100644 index 0000000..6c1c0e5 --- /dev/null +++ b/src/components/SearchBar/SearchBar.tsx @@ -0,0 +1,22 @@ + +interface SearchBarProps { + search: string + onSearchChange: (value: string) => void +} + +function SearchBar({ search, onSearchChange }: SearchBarProps) { + return ( + <> + + onSearchChange(event.target.value)} + /> + + ) +} + +export default SearchBar \ No newline at end of file diff --git a/src/pages/CataloguePage.tsx b/src/pages/CataloguePage.tsx index 74c90b9..b9c4df4 100644 --- a/src/pages/CataloguePage.tsx +++ b/src/pages/CataloguePage.tsx @@ -5,6 +5,10 @@ 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' + function CataloguePage() { const albums: Album[] = albumData as Album[] @@ -14,39 +18,39 @@ function CataloguePage() { const [rating, setRating] = useState(null) const [filtersVisible, setFiltersVisible] = useState(true) - -const genres = [ - 'All genres', - 'Rock', - 'Jazz', - 'Pop', - 'Country', - 'HipHop', - 'Tekno', - 'Indie', - 'RnB', - 'Soul', - 'Kpop', - 'Funk', - 'Blues', - 'Classic', - 'Metal', -] - -const decades = [ - 'All decades', - '1920s', - '1930s', - '1940s', - '1950s', - '1960s', - '1970s', - '1980s', - '1990s', - '2000s', - '2010s', - '2020s', -] + const genres = [ + 'All genres', + 'Rock', + 'Jazz', + 'Pop', + 'Country', + 'HipHop', + 'Tekno', + 'Indie', + 'RnB', + 'Soul', + 'Kpop', + 'Funk', + 'Blues', + 'Classic', + 'Metal', + 'Electronic', + ] + + const decades = [ + 'All decades', + '1920s', + '1930s', + '1940s', + '1950s', + '1960s', + '1970s', + '1980s', + '1990s', + '2000s', + '2010s', + '2020s', + ] const filteredAlbums = useMemo(() => { return albums.filter((album) => { @@ -62,15 +66,29 @@ const decades = [ const matchesDecade = decade === 'All decades' || albumDecade === decade - // Rating er foreløpig bare en UI-kontroll, siden mock-dataene - // ikke inneholder faktiske ratingverdier. + // Rating er kun en visuell kontroll inntil rating-data finnes. const matchesRating = rating === null - return matchesSearch && matchesGenre && matchesDecade && matchesRating + return ( + matchesSearch && + matchesGenre && + matchesDecade && + matchesRating + ) }) }, [albums, search, genre, decade, rating]) - const visibleAlbums = filteredAlbums.slice(0, filtersVisible ? 6 : 8) + const visibleAlbums = filteredAlbums.slice( + 0, + filtersVisible ? 6 : 8, + ) + + function clearFilters() { + setSearch('') + setGenre('All genres') + setDecade('All decades') + setRating(null) + } return (
@@ -83,7 +101,10 @@ const decades = [ - @@ -93,7 +114,9 @@ const decades = [

YOUR MUSIC, YOUR COLLECTION

-

Explore albums.

+

+ Explore albums. +

-
+
{filtersVisible && ( )} -
+

{filteredAlbums.length}{' '} @@ -193,46 +199,7 @@ const decades = [ CURATED FOR YOU

- {visibleAlbums.length > 0 ? ( -
- {visibleAlbums.map((album) => ( - - {album.coverUrl ? ( - {`${album.title} - ) : ( -
- C. - - {album.title} - -
- )} - -
-

{album.title}

-

{album.artist}

-
- {album.releaseYear} - {album.genre.join(' / ')} -
-
- - ))} -
- ) : ( -
-

No albums found

-

Try another search or clear your filters.

-
- )} +