From 78365a3faab9d266f04fba21b6b12547fb49c6d9 Mon Sep 17 00:00:00 2001 From: Tobias Hegge Date: Mon, 14 Sep 2026 18:54:24 +0200 Subject: [PATCH 1/6] added song detail page and made header a component for easy reuse --- webdev-prosjekt1/src/App.css | 65 ------- webdev-prosjekt1/src/App.tsx | 38 ++-- webdev-prosjekt1/src/components/Header.css | 61 +++++++ webdev-prosjekt1/src/components/Header.tsx | 19 ++ webdev-prosjekt1/src/components/Searchbar.tsx | 9 +- .../src/components/SongDetailPage.css | 172 ++++++++++++++++++ .../src/components/SongDetailPage.tsx | 65 +++++++ 7 files changed, 345 insertions(+), 84 deletions(-) create mode 100644 webdev-prosjekt1/src/components/Header.css create mode 100644 webdev-prosjekt1/src/components/Header.tsx create mode 100644 webdev-prosjekt1/src/components/SongDetailPage.css create mode 100644 webdev-prosjekt1/src/components/SongDetailPage.tsx diff --git a/webdev-prosjekt1/src/App.css b/webdev-prosjekt1/src/App.css index 549c75c..a1c9aa0 100644 --- a/webdev-prosjekt1/src/App.css +++ b/webdev-prosjekt1/src/App.css @@ -17,63 +17,6 @@ body { overflow: hidden; } -.main-header { - max-width: 1240px; - margin: 0 auto; - padding: 18px 48px 14px; - border-bottom: 2px solid rgba(212, 175, 55, 0.22); -} - -.app-title { - color: var(--primary_color); - text-decoration: none; - display: inline-flex; - align-items: stretch; - position: relative; - padding-left: 70px; -} - -.app-title-text-wrapper { - display: flex; - flex-direction: column; -} - -.app-logo-wrapper { - position: absolute; - left: 0; - top: 0; - - height: 100%; - margin: var(--margin); -} - -.app-logo { - display: block; - - height: 100%; - width: auto; - - object-fit: contain; -} - -.app-title-text { - font-size: 48px; - line-height: 0.95; - margin: var(--margin); -} - -.header-note { - font-size: 13px; - margin: 4px 0 0; - - font-weight: 400; - letter-spacing: 0.08em; - - text-transform: uppercase; - white-space: nowrap; - text-align: center; -} - .search-section { display: flex; gap: 10px; @@ -137,14 +80,6 @@ body { @media (max-width: 700px) { - .main-header { - padding: 16px 24px 12px; - } - - .app-title-text { - font-size: 40px; - } - .search-controls { width: calc(100% - 48px); } diff --git a/webdev-prosjekt1/src/App.tsx b/webdev-prosjekt1/src/App.tsx index 33ddc37..cd3b838 100644 --- a/webdev-prosjekt1/src/App.tsx +++ b/webdev-prosjekt1/src/App.tsx @@ -1,34 +1,36 @@ import './App.css'; import './variables.css'; +import { useState } from 'react'; import { Button } from './components/Button'; import { Searchbar } from './components/Searchbar'; +import { SongDetailPage, type DetailItem, type DetailSelectHandler } from './components/SongDetailPage'; +import { Header } from './components/Header'; import filterIcon from './assets/filter.png'; import musicWizImage from './assets/musicwiz.png'; -import logoImage from './assets/music-wiz-logo.png'; function App() { + const [selectedItem, setSelectedItem] = useState(null); + const handleResultClick: DetailSelectHandler = (item) => setSelectedItem(item); + const isSongDetailRoute = window.location.pathname === '/song-detail' || window.location.pathname === '/song-detail/'; + const dummyDetailItem: DetailItem = { + type: 'song', + title: 'Song Name', + subtitle: 'Artist Name', + meta: 'Album Name · 2026', + imageLabel: 'Album cover image', + }; + + if (selectedItem || isSongDetailRoute) { + return ; + } + return ( <>
-
- - -
- -
- -

MUSIC WIZ

-

Your personal music magician

-
-
-
+
- +
diff --git a/webdev-prosjekt1/src/components/Header.css b/webdev-prosjekt1/src/components/Header.css new file mode 100644 index 0000000..f19f58f --- /dev/null +++ b/webdev-prosjekt1/src/components/Header.css @@ -0,0 +1,61 @@ +.main-header { + max-width: 1240px; + margin: 0 auto; + padding: 18px 48px 14px; + border-bottom: 2px solid rgba(212, 175, 55, 0.22); +} + +.app-title { + color: var(--primary_color); + text-decoration: none; + display: inline-flex; + align-items: stretch; + position: relative; + padding-left: 70px; +} + +.app-title-text-wrapper { + display: flex; + flex-direction: column; +} + +.app-logo-wrapper { + position: absolute; + left: 0; + top: 0; + height: 100%; + margin: var(--margin); +} + +.app-logo { + display: block; + height: 100%; + width: auto; + object-fit: contain; +} + +.app-title-text { + font-size: 48px; + line-height: 0.95; + margin: var(--margin); +} + +.header-note { + font-size: 13px; + margin: 4px 0 0; + font-weight: 400; + letter-spacing: 0.08em; + text-transform: uppercase; + white-space: nowrap; + text-align: center; +} + +@media (max-width: 700px) { + .main-header { + padding: 16px 24px 12px; + } + + .app-title-text { + font-size: 40px; + } +} \ No newline at end of file diff --git a/webdev-prosjekt1/src/components/Header.tsx b/webdev-prosjekt1/src/components/Header.tsx new file mode 100644 index 0000000..faa2d1a --- /dev/null +++ b/webdev-prosjekt1/src/components/Header.tsx @@ -0,0 +1,19 @@ +import './Header.css'; +import '../variables.css'; +import logoImage from '../assets/music-wiz-logo.png'; + +export const Header = () => { + return ( +
+ +
+ +
+ +

MUSIC WIZ

+

Your personal music magician

+
+
+
+ ); +}; \ No newline at end of file diff --git a/webdev-prosjekt1/src/components/Searchbar.tsx b/webdev-prosjekt1/src/components/Searchbar.tsx index d07b068..96a3553 100644 --- a/webdev-prosjekt1/src/components/Searchbar.tsx +++ b/webdev-prosjekt1/src/components/Searchbar.tsx @@ -1,7 +1,14 @@ import './Searchbar.css'; import '../variables.css'; +import type { DetailSelectHandler } from './SongDetailPage'; + +type SearchbarProps = { + onResultClick?: DetailSelectHandler; +}; + +export const Searchbar = ({ onResultClick }: SearchbarProps) => { + void onResultClick; -export const Searchbar = () => { return (
- -
-
- A musical wizard -
diff --git a/webdev-prosjekt1/src/components/SongDetailPage.css b/webdev-prosjekt1/src/components/SongDetailPage.css index 279f022..a5ba47d 100644 --- a/webdev-prosjekt1/src/components/SongDetailPage.css +++ b/webdev-prosjekt1/src/components/SongDetailPage.css @@ -7,9 +7,9 @@ .detail-content { display: grid; - grid-template-columns: minmax(240px, 360px) minmax(0, 1fr) minmax(130px, 220px); + grid-template-columns: minmax(240px, 320px) minmax(0, 1fr); align-items: center; - gap: clamp(40px, 8vw, 120px); + gap: clamp(28px, 5vw, 72px); max-width: 1100px; margin: 0 auto; padding: 92px 48px 40px; @@ -52,6 +52,7 @@ .detail-copy { max-width: 560px; + min-width: 0; } .detail-eyebrow { @@ -68,7 +69,9 @@ font-size: clamp(2.6rem, 6vw, 5.4rem); line-height: 0.98; font-weight: 700; - word-break: break-word; + word-break: normal; + hyphens: none; + overflow-wrap: break-word; } .detail-subtitle { @@ -96,32 +99,6 @@ font-size: 0.95rem; } -.detail-wizard { - position: relative; - align-self: end; - margin: 0; -} - -.detail-wizard img { - position: relative; - z-index: 1; - display: block; - width: 100%; - filter: drop-shadow(0 18px 20px rgba(0, 0, 0, 0.45)); -} - -.detail-wizard-glow { - position: absolute; - left: 50%; - top: 44%; - width: 170px; - aspect-ratio: 1; - border-radius: 50%; - background: rgba(226, 157, 38, 0.15); - filter: blur(35px); - transform: translate(-50%, -50%); -} - .lyrics-section { display: grid; grid-template-columns: minmax(180px, 260px) minmax(0, 660px); @@ -148,7 +125,7 @@ margin: 0 0 28px; } -@media (max-width: 700px) { +@media (max-width: 900px) { .detail-content { grid-template-columns: 1fr; gap: 54px; @@ -160,8 +137,9 @@ margin: 0 auto; } - .detail-wizard { - width: 180px; + .detail-copy { + min-width: 0; + max-width: 560px; margin: 0 auto; } diff --git a/webdev-prosjekt1/src/components/SongDetailPage.tsx b/webdev-prosjekt1/src/components/SongDetailPage.tsx index 6bd09f8..24d0c65 100644 --- a/webdev-prosjekt1/src/components/SongDetailPage.tsx +++ b/webdev-prosjekt1/src/components/SongDetailPage.tsx @@ -1,7 +1,6 @@ import './SongDetailPage.css'; import '../variables.css'; import { Header } from './Header'; -import musicWizImage from '../assets/musicwiz.png'; import type { Track } from '../api/types'; type SongDetailPageProps = { @@ -45,11 +44,6 @@ export const SongDetailPage = ({ song }: SongDetailPageProps) => {
- -
-
- A musical wizard -
From 8b33c1d9354d3ce7886ea1c703e8d9bbeb9433c9 Mon Sep 17 00:00:00 2001 From: Tobias Hegge Date: Tue, 15 Sep 2026 10:09:28 +0200 Subject: [PATCH 5/6] added artist detail page --- webdev-prosjekt1/src/App.tsx | 21 ++- .../src/components/ArtistDetailPage.css | 142 ++++++++++++++++++ .../src/components/ArtistDetailPage.tsx | 44 ++++++ 3 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 webdev-prosjekt1/src/components/ArtistDetailPage.css create mode 100644 webdev-prosjekt1/src/components/ArtistDetailPage.tsx diff --git a/webdev-prosjekt1/src/App.tsx b/webdev-prosjekt1/src/App.tsx index eeb1781..d38265f 100644 --- a/webdev-prosjekt1/src/App.tsx +++ b/webdev-prosjekt1/src/App.tsx @@ -5,8 +5,9 @@ import { Button } from './components/Button'; import { Searchbar } from './components/Searchbar'; import { SongDetailPage, type DetailSelectHandler } from './components/SongDetailPage'; import { AlbumDetailPage } from './components/AlbumDetailPage'; +import { ArtistDetailPage } from './components/ArtistDetailPage'; import { Header } from './components/Header'; -import type { Album, Track } from './api/types'; +import type { Album, Artist, Track } from './api/types'; import filterIcon from './assets/filter.png'; import musicWizImage from './assets/musicwiz.png'; @@ -15,17 +16,19 @@ function App() { const handleResultClick: DetailSelectHandler = (song) => setSelectedSong(song); const isSongDetailRoute = window.location.pathname === '/song-detail' || window.location.pathname === '/song-detail/'; const isAlbumDetailRoute = window.location.pathname === '/album-detail' || window.location.pathname === '/album-detail/'; - const dummyArtist = { + const isArtistDetailRoute = window.location.pathname === '/artist-detail' || window.location.pathname === '/artist-detail/'; + const dummyAlbumImage = { + url: musicWizImage, + height: 640, + width: 640, + }; + const dummyArtist: Artist = { id: 1, name: 'Artist Name', type: 'artist', href: 'https://api.spotify.com/v1/artists/artist-id', uri: 'spotify:artist:artist-id', - }; - const dummyAlbumImage = { - url: musicWizImage, - height: 640, - width: 640, + images: [dummyAlbumImage], }; const dummySong: Track = { id: 'song-id', @@ -67,6 +70,10 @@ function App() { return ; } + if (isArtistDetailRoute) { + return ; + } + if (selectedSong || isSongDetailRoute) { return ; } diff --git a/webdev-prosjekt1/src/components/ArtistDetailPage.css b/webdev-prosjekt1/src/components/ArtistDetailPage.css new file mode 100644 index 0000000..1bc8deb --- /dev/null +++ b/webdev-prosjekt1/src/components/ArtistDetailPage.css @@ -0,0 +1,142 @@ +.artist-detail-page { + min-height: 100vh; + padding-bottom: 80px; + color: #f8f0df; + background: radial-gradient(circle at 26% 44%, #43331e 0, #1c1814 31%, #11100f 70%); +} + +.artist-detail-page .detail-content { + display: grid; + grid-template-columns: minmax(240px, 320px) minmax(0, 1fr); + align-items: center; + gap: clamp(28px, 5vw, 72px); + max-width: 1100px; + margin: 0 auto; + padding: 92px 48px 40px; +} + +.artist-detail-page .detail-art { + display: grid; + place-items: center; + position: relative; + width: 100%; + margin: 0; + aspect-ratio: 1; + border: 1px solid rgba(212, 175, 55, 0.42); + border-radius: 50%; + overflow: hidden; + background: rgba(17, 16, 15, 0.72); + box-shadow: 18px 22px 0 rgba(212, 175, 55, 0.08), 0 24px 50px rgba(0, 0, 0, 0.36); +} + +.artist-detail-page .detail-art::after { + content: ''; + position: absolute; + inset: 13px; + border: 1px solid rgba(212, 175, 55, 0.2); + border-radius: 50%; +} + +.artist-detail-page .detail-art img { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + object-fit: cover; +} + +.artist-detail-page .detail-art-placeholder { + color: var(--primary_color); + font-size: clamp(1.8rem, 4vw, 3.2rem); + font-weight: 700; + font-family: Georgia, serif; + text-align: center; +} + +.artist-detail-page .detail-copy { + max-width: 560px; + min-width: 0; +} + +.artist-detail-page .detail-eyebrow { + margin: 0 0 12px; + color: var(--primary_color); + font-size: 13px; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.artist-detail-page .detail-copy h1 { + margin: 0; + font-size: clamp(2.6rem, 6vw, 5.4rem); + line-height: 0.98; + font-weight: 700; + word-break: normal; + hyphens: none; + overflow-wrap: break-word; +} + +.artist-detail-page .detail-subtitle { + margin: 22px 0 0; + font-size: clamp(1.05rem, 2vw, 1.35rem); + color: #d9cdb8; +} + +.artist-detail-page .detail-meta { + margin: 12px 0 0; + color: #a79a86; + text-transform: capitalize; +} + +.artist-detail-page .detail-divider { + width: 100%; + height: 1px; + margin: 36px 0 18px; + border: 0; + background: rgba(212, 175, 55, 0.32); +} + +.artist-content { + max-width: 1000px; + margin: 30px auto 0; + padding: 56px 48px 0; + border-top: 1px solid rgba(212, 175, 55, 0.32); +} + +.artist-content h2 { + margin: 0; + font-size: clamp(2rem, 4vw, 3.4rem); + line-height: 1; +} + +.artist-content p { + max-width: 660px; + margin: 28px 0 0; + color: #d9cdb8; + font-size: 1.05rem; + line-height: 1.9; +} + +@media (max-width: 900px) { + .artist-detail-page .detail-content { + grid-template-columns: 1fr; + gap: 54px; + padding: 56px 32px 32px; + } + + .artist-detail-page .detail-art { + max-width: 340px; + margin: 0 auto; + } + + .artist-detail-page .detail-copy { + max-width: 560px; + margin: 0 auto; + } + + .artist-content { + margin-top: 30px; + padding: 42px 32px 0; + } +} diff --git a/webdev-prosjekt1/src/components/ArtistDetailPage.tsx b/webdev-prosjekt1/src/components/ArtistDetailPage.tsx new file mode 100644 index 0000000..bae8536 --- /dev/null +++ b/webdev-prosjekt1/src/components/ArtistDetailPage.tsx @@ -0,0 +1,44 @@ +import './ArtistDetailPage.css'; +import '../variables.css'; +import { Header } from './Header'; +import type { Artist } from '../api/types'; + +type ArtistDetailPageProps = { + artist: Artist; +}; + +export const ArtistDetailPage = ({ artist }: ArtistDetailPageProps) => { + const artistImage = artist.images[0]?.url; + + return ( +
+
+ +
+
+ {artistImage ? ( + {`Portrait + ) : ( + Artist image + )} +
+ +
+

Artist

+

{artist.name}

+

Artist profile

+

{artist.type} · Music Wiz profile

+ +
+
+
+ +
+
+

Artist overview

+
+

Albums, songs and more from this artist will appear here when the artist data is connected.

+
+
+ ); +}; From 17786b22ea96405ba5d56eb8a34f52a7a7377d77 Mon Sep 17 00:00:00 2001 From: Tobias Hegge Date: Tue, 15 Sep 2026 10:18:13 +0200 Subject: [PATCH 6/6] changed from deprecated formevent --- webdev-prosjekt1/src/components/Searchbar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webdev-prosjekt1/src/components/Searchbar.tsx b/webdev-prosjekt1/src/components/Searchbar.tsx index 71d48b5..2c2240a 100644 --- a/webdev-prosjekt1/src/components/Searchbar.tsx +++ b/webdev-prosjekt1/src/components/Searchbar.tsx @@ -1,6 +1,6 @@ import './Searchbar.css'; import '../variables.css'; -import { useState, type FormEvent } from 'react'; +import { useState, type SubmitEvent } from 'react'; type SearchbarProps = { onSearch: (query: string) => void; @@ -9,7 +9,7 @@ type SearchbarProps = { export const Searchbar = ({ onSearch }: SearchbarProps) => { const [query, setQuery] = useState(''); - const handleSubmit = (event: FormEvent) => { + const handleSubmit = (event: SubmitEvent) => { event.preventDefault(); if (!query.trim()) return;