Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions webdev-prosjekt1/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webdev-prosjekt1/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
"@tanstack/query-sync-storage-persister": "^5.102.8",
"@phosphor-icons/react": "^2.1.10",
"@tanstack/react-query": "^5.102.8",
"@tanstack/react-query-persist-client": "^5.102.8",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
Expand Down
65 changes: 42 additions & 23 deletions webdev-prosjekt1/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ body {

.app-shell {
min-height: 100vh;
background: var(--page_background_gradient);
background: var(--primary_background_color);
overflow-x: hidden;
}

Expand Down Expand Up @@ -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 {
Expand All @@ -67,7 +55,7 @@ body {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
gap: 48px;
}

.top-tracks-section {
Expand Down Expand Up @@ -133,6 +121,7 @@ body {
.hero-art {
position: relative;
top: 0;
align-self: center;
}
}

Expand All @@ -146,7 +135,7 @@ body {
position: sticky;
top: 24px;

margin: var(--margin);
margin: 24px 0 0;

width: 520px;
min-height: 460px;
Expand All @@ -161,26 +150,44 @@ body {
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));
}

.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 {
Expand All @@ -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);
Expand All @@ -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 {
Expand Down
31 changes: 26 additions & 5 deletions webdev-prosjekt1/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T,>(key: string): T | null => {
try {
Expand All @@ -27,10 +27,22 @@ const readStoredItem = <T,>(key: string): T | null => {
}
};

const readSessionItem = <T,>(key: string): T | null => {
try {
const storedItem = sessionStorage.getItem(key);
return storedItem ? JSON.parse(storedItem) as T : null;
} catch {
return 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(() => readSessionItem<string>(SEARCH_QUERY_STORAGE_KEY) ?? '');
const [isFilterOpen, setIsFilterOpen] = useState(false);
const [filters, setFilters] = useState(EMPTY_FILTERS);
const [filters, setFilters] = useState(() => readSessionItem<typeof EMPTY_FILTERS>(FILTERS_STORAGE_KEY) ?? EMPTY_FILTERS);
const [currentPath, setCurrentPath] = useState(window.location.pathname);
const [selectedTrack, setSelectedTrack] = useState<Track | null>(() => readStoredItem<Track>('music-wiz-selected-track'));
const [selectedAlbum, setSelectedAlbum] = useState<Album | null>(() => readStoredItem<Album>('music-wiz-selected-album'));
Expand Down Expand Up @@ -97,6 +109,12 @@ function App() {

return () => window.removeEventListener('popstate', handlePopState);
}, []);
useEffect(() => {
sessionStorage.setItem(SEARCH_QUERY_STORAGE_KEY, JSON.stringify(query));
}, [query]);
useEffect(() => {
sessionStorage.setItem(FILTERS_STORAGE_KEY, JSON.stringify(filters));
}, [filters]);
const navigateTo = (path: string) => {
window.history.pushState({}, '', path);
setCurrentPath(path);
Expand Down Expand Up @@ -140,18 +158,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<number, string[]>();
Expand Down Expand Up @@ -216,7 +237,7 @@ function App() {
<section className='search-section'>
<Searchbar onSearch={handleSearch} initialQuery={query} />
<Button
content={<><img src={filterIcon} alt='' /> <span>Filter</span></>}
content={<><FunnelSimple size={17} weight='bold' aria-hidden='true' /> <span>Filter</span></>}
onClick={() => setIsFilterOpen(true)}
/>
</section>
Expand Down Expand Up @@ -260,7 +281,7 @@ function App() {
</section>
</aside>
<figure className='hero-art'>
<span className='art-glow' aria-hidden='true' />
<span className='music-wiz-glow' aria-hidden='true' />
<img src={musicWizImage} alt='A musical wizard' />
</figure>
</section>
Expand Down
15 changes: 5 additions & 10 deletions webdev-prosjekt1/src/api/spotify.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -88,8 +92,6 @@ export async function search(query: string): Promise<SearchResult> {
}
const searchResult: SearchResult = newSearchResult;

console.log(json);

return searchResult;
}

Expand Down Expand Up @@ -140,8 +142,6 @@ export async function fetchTracks(ids: string[]): Promise<Track[]> {
tracks.push(track);
}

console.log(tracks);

return tracks;
}

Expand Down Expand Up @@ -176,12 +176,7 @@ export async function fetchAlbumsByArtistId(id: string): Promise<Album[]> {
});

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();
Expand Down
16 changes: 8 additions & 8 deletions webdev-prosjekt1/src/components/AlbumCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ export const AlbumCard = ({ album, onClick }: AlbumCardProps) => {
<li className="search-result-card" role='button' tabIndex={0} onClick={onClick} onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') onClick();
}}>
<div className='card-image-wrapper'>
<figure className='card-image-wrapper'>
<img src={image} alt="" className='card-image'/>
</div>
</figure>

<div className='card-info-container'>
<div className='title-and-artist'>
<section className='card-info-container'>
<header className='title-and-artist'>
<h3 className='card-header'>{album.name}</h3>
<span className='artist-name'>{artistNames}</span>
</div>
<div className='duration-display-container'>
</header>
<footer className='duration-display-container'>
<span className='duration-display'>{year}</span>
</div>
</div>
</footer>
</section>
</li>
)
}
5 changes: 3 additions & 2 deletions webdev-prosjekt1/src/components/AlbumDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -43,7 +44,7 @@ export const AlbumDetailPage = ({ album, onTrackClick }: AlbumDetailPageProps) =
<p className='detail-meta'>
{album.album_type} · {album.release_date} · {album.total_tracks} tracks
</p>
<PreviewPlayer key={album.id} tracks={tracks ?? []} href={album.href} onTrackChange={setPlayingTrackId} />
<PreviewPlayer key={album.id} tracks={tracks ?? []} spotifyUri={album.uri} onTrackChange={setPlayingTrackId} />

<hr className='detail-divider' />
</header>
Expand Down
Loading