From 5398c3a57a227d9e5f497263ad3089c24fc7c814 Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Tue, 15 Sep 2026 20:44:10 +0200 Subject: [PATCH 1/5] Added more persistent caching of api calls. --- webdev-prosjekt1/package-lock.json | 46 +++++++++++++++++++ webdev-prosjekt1/package.json | 2 + webdev-prosjekt1/src/App.tsx | 3 ++ webdev-prosjekt1/src/api/spotify.ts | 11 +---- .../src/components/AlbumDetailPage.tsx | 3 +- .../src/components/ArtistDetailPage.tsx | 6 ++- .../src/components/TrackDetailPage.tsx | 3 +- webdev-prosjekt1/src/main.tsx | 27 +++++++++-- 8 files changed, 83 insertions(+), 18 deletions(-) diff --git a/webdev-prosjekt1/package-lock.json b/webdev-prosjekt1/package-lock.json index d64e3da..82a0690 100644 --- a/webdev-prosjekt1/package-lock.json +++ b/webdev-prosjekt1/package-lock.json @@ -8,7 +8,9 @@ "name": "webdev-prosjekt1", "version": "0.0.0", "dependencies": { + "@tanstack/query-sync-storage-persister": "^5.102.8", "@tanstack/react-query": "^5.102.8", + "@tanstack/react-query-persist-client": "^5.102.8", "react": "^19.2.8", "react-dom": "^19.2.8" }, @@ -828,6 +830,33 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/query-persist-client-core": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.102.8.tgz", + "integrity": "sha512-t3v4/D6ejo/BrPzM5gm/AT3c4CWPCxFa+0cIjAeHAeanGPG6rCjFS1hZnPZH/rf55qe3qYYgx5U6iBZMY1PQjA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.102.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-sync-storage-persister": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.102.8.tgz", + "integrity": "sha512-UvIpYl6jipKJHfITAieQaXMvVPoJh2IkQkWDYZNRPycoGbNYdqB+UdRVoXkZagB3EdeKnlC7sA61grsEC87Wfw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.102.8", + "@tanstack/query-persist-client-core": "5.102.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/react-query": { "version": "5.102.8", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.102.8.tgz", @@ -844,6 +873,23 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-persist-client": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.102.8.tgz", + "integrity": "sha512-WOZReC+m9klekPtG7eeZ3txJeMA9lg2R0RytSG3yXcZgJItQAVIyoobJdbdIQtlpgcU2tSzsOEBtJacs2vlCOA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-persist-client-core": "5.102.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.102.8", + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", diff --git a/webdev-prosjekt1/package.json b/webdev-prosjekt1/package.json index 76c7624..935cc90 100644 --- a/webdev-prosjekt1/package.json +++ b/webdev-prosjekt1/package.json @@ -10,7 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@tanstack/query-sync-storage-persister": "^5.102.8", "@tanstack/react-query": "^5.102.8", + "@tanstack/react-query-persist-client": "^5.102.8", "react": "^19.2.8", "react-dom": "^19.2.8" }, diff --git a/webdev-prosjekt1/src/App.tsx b/webdev-prosjekt1/src/App.tsx index 510a04d..7ee3085 100644 --- a/webdev-prosjekt1/src/App.tsx +++ b/webdev-prosjekt1/src/App.tsx @@ -131,18 +131,21 @@ function App() { queryFn: () => search(effectiveQuery), enabled: query.trim().length > 0, staleTime: 1000 * 60 * 60, + gcTime: 1000 * 60 * 60, }); const { data: topTracks = [] } = useQuery({ queryKey: ['top-10-tracks'], queryFn: () => fetchTracks(TOP_10_IDS), staleTime: Infinity, + gcTime: Infinity, }); const { data: bottomTracks = [] } = useQuery({ queryKey: ['bottom-10-tracks'], queryFn: () => fetchTracks(BOTTOM_10_IDS), staleTime: Infinity, + gcTime: Infinity, }); const artistGenresById = new Map(); diff --git a/webdev-prosjekt1/src/api/spotify.ts b/webdev-prosjekt1/src/api/spotify.ts index 52b84e9..cea5b77 100644 --- a/webdev-prosjekt1/src/api/spotify.ts +++ b/webdev-prosjekt1/src/api/spotify.ts @@ -88,8 +88,6 @@ export async function search(query: string): Promise { } const searchResult: SearchResult = newSearchResult; - console.log(json); - return searchResult; } @@ -140,8 +138,6 @@ export async function fetchTracks(ids: string[]): Promise { tracks.push(track); } - console.log(tracks); - return tracks; } @@ -176,12 +172,7 @@ export async function fetchAlbumsByArtistId(id: string): Promise { }); if (!response.ok) { - const error = await response.text(); - console.error("Spotify response body:", error); - - throw new Error( - `Spotify API error: ${response.status} ${response.statusText}` - ); + throw new Error(`Spotify API error: ${response.status} ${response.statusText}`); } const json = await response.json(); diff --git a/webdev-prosjekt1/src/components/AlbumDetailPage.tsx b/webdev-prosjekt1/src/components/AlbumDetailPage.tsx index 726c4e5..e478405 100644 --- a/webdev-prosjekt1/src/components/AlbumDetailPage.tsx +++ b/webdev-prosjekt1/src/components/AlbumDetailPage.tsx @@ -20,7 +20,8 @@ export const AlbumDetailPage = ({ album, onTrackClick }: AlbumDetailPageProps) = const { data: tracks, isLoading, isError } = useQuery({ queryKey: ['album-tracks', album.id], queryFn: () => fetchAlbumTracks(album.id), - staleTime: 1000 * 60 * 60 + staleTime: 1000 * 60 * 60, + gcTime: 1000 * 60 * 60, }); return ( diff --git a/webdev-prosjekt1/src/components/ArtistDetailPage.tsx b/webdev-prosjekt1/src/components/ArtistDetailPage.tsx index cc380ce..013fd42 100644 --- a/webdev-prosjekt1/src/components/ArtistDetailPage.tsx +++ b/webdev-prosjekt1/src/components/ArtistDetailPage.tsx @@ -18,12 +18,14 @@ export const ArtistDetailPage = ({ artist, onTrackClick, onAlbumClick }: ArtistD const { data: tracks, isLoading: isLoadingTracks, isError: isTracksError } = useQuery({ queryKey: ['artist-tracks', artist.name], queryFn: () => fetchTracksByArtist(artist.name), - staleTime: 1000 * 60 * 60 + staleTime: 1000 * 60 * 60, + gcTime: 1000 * 60 * 60, }); const { data: albums, isLoading: isLoadingAlbums, isError: isAlbumsError } = useQuery({ queryKey: ['artist-albums', artist.id], queryFn: () => fetchAlbumsByArtistId(String(artist.id)), - staleTime: 1000 * 60 * 60 + staleTime: 1000 * 60 * 60, + gcTime: 1000 * 60 * 60, }); return ( diff --git a/webdev-prosjekt1/src/components/TrackDetailPage.tsx b/webdev-prosjekt1/src/components/TrackDetailPage.tsx index 0bdd1dc..97c010f 100644 --- a/webdev-prosjekt1/src/components/TrackDetailPage.tsx +++ b/webdev-prosjekt1/src/components/TrackDetailPage.tsx @@ -29,7 +29,8 @@ export const TrackDetailPage = ({ song, onAlbumClick }: TrackDetailPageProps) => queryKey: ['lyrics', song.id], queryFn: () => getLyrics(firstArtistName, song.name, song.album.name, song.duration_ms), enabled: Boolean(firstArtistName && song.name && song.album.name), - staleTime: Infinity + staleTime: Infinity, + gcTime: Infinity, }); return ( diff --git a/webdev-prosjekt1/src/main.tsx b/webdev-prosjekt1/src/main.tsx index e534854..02cf3a1 100644 --- a/webdev-prosjekt1/src/main.tsx +++ b/webdev-prosjekt1/src/main.tsx @@ -1,16 +1,35 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { QueryClient } from "@tanstack/react-query" +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' +import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' import './index.css' import './variables.css' import App from './App.tsx' -const queryClient = new QueryClient() +const persistedQueryKeys = new Set(['top-10-tracks', 'bottom-10-tracks']) +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: Infinity, + }, + }, +}) +const persister = createSyncStoragePersister({ storage: window.localStorage }) createRoot(document.getElementById('root')!).render( - + persistedQueryKeys.has(String(query.queryKey[0])), + }, + }} + > - + , ) From 16a71c78f2654e9421e1a18e037a1613c8237ba1 Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Tue, 15 Sep 2026 21:33:45 +0200 Subject: [PATCH 2/5] Fixed the bad navigation in the header. --- webdev-prosjekt1/src/components/Header.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/webdev-prosjekt1/src/components/Header.tsx b/webdev-prosjekt1/src/components/Header.tsx index 0c512d0..e2a9bc4 100644 --- a/webdev-prosjekt1/src/components/Header.tsx +++ b/webdev-prosjekt1/src/components/Header.tsx @@ -1,5 +1,6 @@ import './Header.css'; import '../variables.css'; +import type { MouseEvent } from 'react'; import logoImage from '../assets/music-wiz-logo.png'; type HeaderProps = { @@ -7,9 +8,23 @@ type HeaderProps = { }; export const Header = ({ onHome }: HeaderProps) => { + const navigateTo = (path: string) => { + window.history.pushState({}, '', path); + window.dispatchEvent(new PopStateEvent('popstate')); + }; + const handleHomeClick = (event: MouseEvent) => { + event.preventDefault(); + onHome?.(); + navigateTo('/'); + }; + const handleFavoritesClick = (event: MouseEvent) => { + event.preventDefault(); + navigateTo('/favorites'); + }; + return (
- +
@@ -18,7 +33,7 @@ export const Header = ({ onHome }: HeaderProps) => {

Your personal music magician

- Favorites + Favorites
); }; \ No newline at end of file From 63e988d5f094081107fab02d08504c7ec58cf2db Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Tue, 15 Sep 2026 23:23:37 +0200 Subject: [PATCH 3/5] Made the Favorites and Homepage look better in different screen sizes. --- webdev-prosjekt1/src/App.css | 65 ++++++++++++------- webdev-prosjekt1/src/App.tsx | 2 +- .../src/components/FavoritesPage.css | 33 ++++++++-- .../src/components/FavoritesPage.tsx | 1 + webdev-prosjekt1/src/components/TrackCard.css | 18 +++++ 5 files changed, 91 insertions(+), 28 deletions(-) diff --git a/webdev-prosjekt1/src/App.css b/webdev-prosjekt1/src/App.css index 89b41bd..26a2f74 100644 --- a/webdev-prosjekt1/src/App.css +++ b/webdev-prosjekt1/src/App.css @@ -10,7 +10,7 @@ body { .app-shell { min-height: 100vh; - background: var(--page_background_gradient); + background: var(--primary_background_color); overflow-x: hidden; } @@ -43,18 +43,6 @@ body { width: calc(100% - 48px); } - .hero-art { - width: 320px; - min-height: 380px; - } - - .hero-art img { - max-width: 290px; - } - - .art-glow { - width: 230px; - } } .hero-section { @@ -67,7 +55,7 @@ body { display: flex; align-items: flex-start; justify-content: space-between; - gap: 24px; + gap: 48px; } .top-tracks-section { @@ -133,6 +121,7 @@ body { .hero-art { position: relative; top: 0; + align-self: center; } } @@ -146,7 +135,7 @@ body { position: sticky; top: 24px; - margin: var(--margin); + margin: 24px 0 0; width: 520px; min-height: 460px; @@ -161,8 +150,8 @@ body { position: relative; z-index: 1; - width: 100%; - max-width: 480px; + width: 480px; + max-width: calc(100% - 40px); height: auto; object-fit: contain; @@ -170,17 +159,35 @@ body { filter: drop-shadow(0 20px 25px var(--shadow_hero)); } -.art-glow { +.music-wiz-glow { position: absolute; - - width: 300px; + top: 50%; + left: 50%; + width: min(125%, 640px); aspect-ratio: 1; - border-radius: 50%; - + transform: translate(-50%, -50%); background: var(--hero_glow); + filter: blur(50px); + pointer-events: none; +} - filter: blur(45px); +/* Kept after the desktop artwork rules so the homepage matches favorites on phones. */ +@media (max-width: 700px) { + .hero-art { + width: 320px; + min-height: 380px; + } + + .hero-art img { + width: 290px; + max-width: calc(100% - 30px); + } + + .hero-art .music-wiz-glow { + width: 380px; + max-width: 125%; + } } .card-list { @@ -207,6 +214,12 @@ body { align-items: start; } +@media (max-width: 1100px) { + .results-section { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + @media (max-width: 700px) { .search-controls { width: calc(100% - 48px); @@ -215,6 +228,12 @@ body { .hero-section { width: calc(100% - 48px); } + + .results-section { + width: calc(100% - 48px); + grid-template-columns: minmax(0, 1fr); + gap: 32px; + } } .results-group { diff --git a/webdev-prosjekt1/src/App.tsx b/webdev-prosjekt1/src/App.tsx index 72a02f0..2d177a4 100644 --- a/webdev-prosjekt1/src/App.tsx +++ b/webdev-prosjekt1/src/App.tsx @@ -263,7 +263,7 @@ function App() {
-
diff --git a/webdev-prosjekt1/src/components/FavoritesPage.css b/webdev-prosjekt1/src/components/FavoritesPage.css index 0413b89..307c9a2 100644 --- a/webdev-prosjekt1/src/components/FavoritesPage.css +++ b/webdev-prosjekt1/src/components/FavoritesPage.css @@ -2,7 +2,7 @@ min-height: 100vh; padding-bottom: 80px; color: var(--text_primary); - background: var(--page_background_gradient); + background: var(--primary_background_color); } .favorites-layout { @@ -58,13 +58,32 @@ .favorites-art img { position: relative; z-index: 1; - width: 100%; - max-width: 480px; + width: 480px; + max-width: calc(100% - 40px); height: auto; object-fit: contain; filter: drop-shadow(0 20px 25px var(--shadow_hero)); } +.favorites-art .music-wiz-glow { + position: absolute; + top: 50%; + left: 50%; + width: min(125%, 640px); + aspect-ratio: 1; + border-radius: 50%; + transform: translate(-50%, -50%); + background: var(--hero_glow); + filter: blur(50px); + pointer-events: none; +} + +@media (min-width: 1101px) { + .favorites-art { + margin-top: 72px; + } +} + @media (max-width: 1100px) { .favorites-layout { flex-direction: column; @@ -102,7 +121,13 @@ } .favorites-art img { - max-width: 290px; + width: 290px; + max-width: calc(100% - 30px); + } + + .favorites-art .music-wiz-glow { + width: 380px; + max-width: 125%; } } diff --git a/webdev-prosjekt1/src/components/FavoritesPage.tsx b/webdev-prosjekt1/src/components/FavoritesPage.tsx index 4accd37..a74822c 100644 --- a/webdev-prosjekt1/src/components/FavoritesPage.tsx +++ b/webdev-prosjekt1/src/components/FavoritesPage.tsx @@ -32,6 +32,7 @@ export const FavoritesPage = ({ onTrackClick }: FavoritesPageProps) => { )}
+
diff --git a/webdev-prosjekt1/src/components/TrackCard.css b/webdev-prosjekt1/src/components/TrackCard.css index 117adad..608cdf5 100644 --- a/webdev-prosjekt1/src/components/TrackCard.css +++ b/webdev-prosjekt1/src/components/TrackCard.css @@ -39,6 +39,8 @@ .card-info-container { display: flex; justify-content: space-between; + flex: 1; + min-width: 0; width: 100%; gap: 1rem; @@ -50,7 +52,9 @@ flex-direction: column; justify-content: center; + flex: 1; min-width: 0; + overflow: hidden; gap: 0.35rem; } @@ -81,13 +85,27 @@ display: flex; align-items: center; justify-content: center; + flex: 0 0 auto; gap: 1rem; + white-space: nowrap; } .duration-display { color: var(--primary_color); } +@media (max-width: 500px) { + .card-info-container { + gap: 0.5rem; + padding-right: 0.6rem; + padding-left: 0.6rem; + } + + .duration-display-container { + gap: 0.5rem; + } +} + .duration-display { color: var(--primary_color); } From 8399ae821272eb35bfa09e56a1968d5991696ec2 Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Wed, 16 Sep 2026 11:08:07 +0200 Subject: [PATCH 4/5] Fixed minor UI issues, and made the page more responsive. (#27) --- webdev-prosjekt1/src/App.tsx | 17 +++++-- webdev-prosjekt1/src/api/spotify.ts | 4 ++ webdev-prosjekt1/src/components/AlbumCard.tsx | 16 +++---- .../src/components/AlbumDetailPage.tsx | 2 +- .../src/components/ArtistCard.tsx | 12 ++--- .../src/components/ArtistDetailPage.tsx | 20 ++++---- .../src/components/FilterPopup.css | 46 ++++++++++++++++--- .../src/components/FilterPopup.tsx | 3 +- webdev-prosjekt1/src/components/Header.css | 12 ++++- .../src/components/PreviewPlayer.tsx | 43 +++++++++-------- webdev-prosjekt1/src/components/Searchbar.css | 4 +- webdev-prosjekt1/src/components/Searchbar.tsx | 3 +- webdev-prosjekt1/src/components/TrackCard.css | 1 + webdev-prosjekt1/src/components/TrackCard.tsx | 16 +++---- .../src/components/TrackDetailPage.tsx | 2 +- 15 files changed, 128 insertions(+), 73 deletions(-) diff --git a/webdev-prosjekt1/src/App.tsx b/webdev-prosjekt1/src/App.tsx index 2d177a4..a24cef7 100644 --- a/webdev-prosjekt1/src/App.tsx +++ b/webdev-prosjekt1/src/App.tsx @@ -15,8 +15,8 @@ import { ArtistCard } from './components/ArtistCard'; import { FilterPopup, EMPTY_FILTERS } from './components/FilterPopup'; import type { Album, AlbumTrack, Artist, Track } from './api/types'; import { fetchTrack, fetchTracks, search, TOP_10_IDS, BOTTOM_10_IDS } from './api/spotify'; -import filterIcon from './assets/filter.png'; import musicWizImage from './assets/musicwiz.png'; +import { FunnelSimple } from '@phosphor-icons/react'; const readStoredItem = (key: string): T | null => { try { @@ -27,10 +27,13 @@ const readStoredItem = (key: string): T | null => { } }; +const SEARCH_QUERY_STORAGE_KEY = 'music-wiz-search-query'; +const FILTERS_STORAGE_KEY = 'music-wiz-filters'; + function App() { - const [query, setQuery] = useState(''); + const [query, setQuery] = useState(() => readStoredItem(SEARCH_QUERY_STORAGE_KEY) ?? ''); const [isFilterOpen, setIsFilterOpen] = useState(false); - const [filters, setFilters] = useState(EMPTY_FILTERS); + const [filters, setFilters] = useState(() => readStoredItem(FILTERS_STORAGE_KEY) ?? EMPTY_FILTERS); const [currentPath, setCurrentPath] = useState(window.location.pathname); const [selectedTrack, setSelectedTrack] = useState(() => readStoredItem('music-wiz-selected-track')); const [selectedAlbum, setSelectedAlbum] = useState(() => readStoredItem('music-wiz-selected-album')); @@ -97,6 +100,12 @@ function App() { return () => window.removeEventListener('popstate', handlePopState); }, []); + useEffect(() => { + localStorage.setItem(SEARCH_QUERY_STORAGE_KEY, JSON.stringify(query)); + }, [query]); + useEffect(() => { + localStorage.setItem(FILTERS_STORAGE_KEY, JSON.stringify(filters)); + }, [filters]); const navigateTo = (path: string) => { window.history.pushState({}, '', path); setCurrentPath(path); @@ -219,7 +228,7 @@ function App() {
diff --git a/webdev-prosjekt1/src/api/spotify.ts b/webdev-prosjekt1/src/api/spotify.ts index cea5b77..6a29a57 100644 --- a/webdev-prosjekt1/src/api/spotify.ts +++ b/webdev-prosjekt1/src/api/spotify.ts @@ -1,5 +1,9 @@ import type { AlbumTrack, SearchResult, Track, Album } from "./types"; +export const spotifyUriToWebUrl = (uri: string): string => { + const match = uri.match(/^spotify:(track|album|artist):([^:?]+)$/); + return match ? `https://open.spotify.com/${match[1]}/${match[2]}` : uri; +}; const BASE_URL = "https://api.spotify.com/v1" const CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID; diff --git a/webdev-prosjekt1/src/components/AlbumCard.tsx b/webdev-prosjekt1/src/components/AlbumCard.tsx index d2fa81c..38ccb01 100644 --- a/webdev-prosjekt1/src/components/AlbumCard.tsx +++ b/webdev-prosjekt1/src/components/AlbumCard.tsx @@ -16,19 +16,19 @@ export const AlbumCard = ({ album, onClick }: AlbumCardProps) => {
  • { if (event.key === 'Enter' || event.key === ' ') onClick(); }}> -
    +
    -
    + -
    -
    +
    +

    {album.name}

    {artistNames} -
    -
    + +
    {year} -
    -
    + +
  • ) } diff --git a/webdev-prosjekt1/src/components/AlbumDetailPage.tsx b/webdev-prosjekt1/src/components/AlbumDetailPage.tsx index e478405..20ccff8 100644 --- a/webdev-prosjekt1/src/components/AlbumDetailPage.tsx +++ b/webdev-prosjekt1/src/components/AlbumDetailPage.tsx @@ -44,7 +44,7 @@ export const AlbumDetailPage = ({ album, onTrackClick }: AlbumDetailPageProps) =

    {album.album_type} · {album.release_date} · {album.total_tracks} tracks

    - +
    diff --git a/webdev-prosjekt1/src/components/ArtistCard.tsx b/webdev-prosjekt1/src/components/ArtistCard.tsx index 101ed9f..26c95d8 100644 --- a/webdev-prosjekt1/src/components/ArtistCard.tsx +++ b/webdev-prosjekt1/src/components/ArtistCard.tsx @@ -15,16 +15,16 @@ export const ArtistCard = ({ artist, onClick }: ArtistCardProps) => {
  • { if (event.key === 'Enter' || event.key === ' ') onClick(); }}> -
    +
    -
    + -
    -
    +
    +

    {artist.name}

    Artist -
    -
    + +
  • ) } diff --git a/webdev-prosjekt1/src/components/ArtistDetailPage.tsx b/webdev-prosjekt1/src/components/ArtistDetailPage.tsx index 013fd42..9c35f03 100644 --- a/webdev-prosjekt1/src/components/ArtistDetailPage.tsx +++ b/webdev-prosjekt1/src/components/ArtistDetailPage.tsx @@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query'; import { Header } from './Header'; import { AlbumCard } from './AlbumCard'; import { TrackCard } from './TrackCard'; -import { fetchAlbumsByArtistId, fetchTracksByArtist } from '../api/spotify'; +import { fetchAlbumsByArtistId, fetchTracksByArtist, spotifyUriToWebUrl } from '../api/spotify'; import type { Album, Artist, Track } from '../api/types'; type ArtistDetailPageProps = { @@ -44,7 +44,7 @@ export const ArtistDetailPage = ({ artist, onTrackClick, onAlbumClick }: ArtistD

    Artist

    {artist.name}

    - + Open in Spotify @@ -52,17 +52,17 @@ export const ArtistDetailPage = ({ artist, onTrackClick, onAlbumClick }: ArtistD
    -
    +

    Artist overview

    -
    +
    -
    +

    Tracks

    {tracks?.length ?? 0} -
    + {isLoadingTracks &&

    Loading songs...

    } {isTracksError &&

    Could not load songs for this artist.

    } {tracks && tracks.length > 0 && ( @@ -73,10 +73,10 @@ export const ArtistDetailPage = ({ artist, onTrackClick, onAlbumClick }: ArtistD
    -
    +

    Albums

    {albums?.length ?? 0} -
    + {isLoadingAlbums &&

    Loading albums...

    } {isAlbumsError &&

    Could not load albums for this artist.

    } {albums && albums.length > 0 && ( @@ -85,8 +85,8 @@ export const ArtistDetailPage = ({ artist, onTrackClick, onAlbumClick }: ArtistD )}
    -
    -
    + + ); }; diff --git a/webdev-prosjekt1/src/components/FilterPopup.css b/webdev-prosjekt1/src/components/FilterPopup.css index 94729a3..3d77238 100644 --- a/webdev-prosjekt1/src/components/FilterPopup.css +++ b/webdev-prosjekt1/src/components/FilterPopup.css @@ -15,6 +15,9 @@ .filter-popup { width: 100%; max-width: 420px; + max-height: calc(100dvh - 24px); + overflow-y: auto; + overscroll-behavior: contain; display: flex; flex-direction: column; @@ -29,6 +32,29 @@ color: #f8f0df; } +@media (orientation: landscape) and (max-height: 600px) { + .filter-popup { + gap: 12px; + padding: 16px; + } + + .filter-field input { + height: 34px; + } + + .filter-genres legend { + padding-bottom: 4px; + } + + .genre-options { + gap: 6px; + } + + .genre-chip { + padding: 5px 12px; + } +} + .filter-popup-header { display: flex; align-items: center; @@ -41,7 +67,13 @@ } .filter-close { + display: grid; + place-items: center; + width: 36px; + height: 36px; + padding: 0; border: none; + border-radius: 10; background: transparent; color: #f8f0df; font-size: 1.4rem; @@ -49,6 +81,12 @@ cursor: pointer; } +.filter-close:hover, +.filter-close:focus-visible { + border-radius: 10; + background: var(--primary_color_hover); +} + .filter-field { display: flex; flex-direction: column; @@ -173,8 +211,7 @@ gap: 10px; } -.filter-clear, -.filter-apply { +.filter-clear { padding: 10px 18px; border-radius: var(--box_border_radius); @@ -190,8 +227,3 @@ background: transparent; color: var(--primary_color); } - -.filter-apply { - background: var(--primary_color); - color: var(--primary_background_color); -} diff --git a/webdev-prosjekt1/src/components/FilterPopup.tsx b/webdev-prosjekt1/src/components/FilterPopup.tsx index 95ef2e1..e5d1a5e 100644 --- a/webdev-prosjekt1/src/components/FilterPopup.tsx +++ b/webdev-prosjekt1/src/components/FilterPopup.tsx @@ -28,7 +28,7 @@ export const FilterPopup = ({ filters, onChange, onClear, onClose }: FilterPopup }; return ( -
    +
    // This is valid use of div, as no other semantic tag fits.
    event.stopPropagation()}>

    Filter

    @@ -96,7 +96,6 @@ export const FilterPopup = ({ filters, onChange, onClear, onClose }: FilterPopup
    -
    diff --git a/webdev-prosjekt1/src/components/Header.css b/webdev-prosjekt1/src/components/Header.css index 2ac84a5..eaaaaf8 100644 --- a/webdev-prosjekt1/src/components/Header.css +++ b/webdev-prosjekt1/src/components/Header.css @@ -1,7 +1,8 @@ .main-header { - display: flex; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; align-items: center; - justify-content: space-between; + column-gap: 24px; max-width: 1240px; margin: 0 auto; padding: 18px 48px 14px; @@ -68,10 +69,17 @@ @media (max-width: 700px) { .main-header { + grid-template-columns: minmax(0, 1fr); + row-gap: 12px; padding: 16px 24px 12px; } .app-title-text { font-size: 40px; } + + .favorites-link { + grid-column: 1; + margin-left: 70px; + } } \ No newline at end of file diff --git a/webdev-prosjekt1/src/components/PreviewPlayer.tsx b/webdev-prosjekt1/src/components/PreviewPlayer.tsx index 64284c5..399000e 100644 --- a/webdev-prosjekt1/src/components/PreviewPlayer.tsx +++ b/webdev-prosjekt1/src/components/PreviewPlayer.tsx @@ -1,5 +1,6 @@ import { useRef, useState } from 'react'; import { fetchPreview } from '../api/itunes'; +import { spotifyUriToWebUrl } from '../api/spotify'; import './SpotifyLink.css'; type PreviewTrack = { @@ -10,11 +11,11 @@ type PreviewTrack = { type PreviewPlayerProps = { tracks: PreviewTrack[]; - href?: string; + spotifyUri?: string; onTrackChange?: (trackId: string | null) => void; }; -export const PreviewPlayer = ({ tracks, href, onTrackChange }: PreviewPlayerProps) => { +export const PreviewPlayer = ({ tracks, spotifyUri, onTrackChange }: PreviewPlayerProps) => { const audioRef = useRef(null); const [currentIndex, setCurrentIndex] = useState(0); const [previewUrls, setPreviewUrls] = useState>({}); @@ -89,23 +90,25 @@ export const PreviewPlayer = ({ tracks, href, onTrackChange }: PreviewPlayerProp }; return ( -
    - - {href && ( - - Open in Spotify - - )} -
    + <> +
    + + {spotifyUri && ( + + Open in Spotify + + )} +
    + ); }; \ No newline at end of file diff --git a/webdev-prosjekt1/src/components/Searchbar.css b/webdev-prosjekt1/src/components/Searchbar.css index 53cda69..9c9c6de 100644 --- a/webdev-prosjekt1/src/components/Searchbar.css +++ b/webdev-prosjekt1/src/components/Searchbar.css @@ -15,12 +15,10 @@ } .search-icon { + flex-shrink: 0; margin-right: 10px; color: var(--primary_color); - - font-size: 1.6rem; - line-height: 1; } .searchbar input { diff --git a/webdev-prosjekt1/src/components/Searchbar.tsx b/webdev-prosjekt1/src/components/Searchbar.tsx index d96903b..051d956 100644 --- a/webdev-prosjekt1/src/components/Searchbar.tsx +++ b/webdev-prosjekt1/src/components/Searchbar.tsx @@ -1,6 +1,7 @@ import './Searchbar.css'; import '../variables.css'; import { useEffect, useState, type SubmitEvent } from 'react'; +import { MagnifyingGlass } from '@phosphor-icons/react'; type SearchbarProps = { onSearch: (query: string) => void; @@ -29,7 +30,7 @@ export const Searchbar = ({ onSearch, initialQuery = '' }: SearchbarProps) => { return (
    - +