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 ? (
+
+ ) : (
+
+ 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 = [
-