diff --git a/frontend/src/components/explore-artist-details/explore-artist-details.ts b/frontend/src/components/explore-artist-details/explore-artist-details.ts index ce8dc04..93e2996 100644 --- a/frontend/src/components/explore-artist-details/explore-artist-details.ts +++ b/frontend/src/components/explore-artist-details/explore-artist-details.ts @@ -5,11 +5,13 @@ import { LookupArtist, BrowseReleaseGroups, TopRecordingsForArtist, + SimilarArtists, } from '@go/explore/Service'; import type { MBArtist, MBReleaseGroup, LBTopRecording, + LBSimilarArtist, } from '@go/explore/Service'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; @@ -67,6 +69,8 @@ export class ExploreArtistDetails extends LitElement { @state() private errorArtist = ''; @state() private errorTracks = ''; @state() private errorReleases = ''; + @state() private similarArtists: LBSimilarArtist[] = []; + @state() private loadingSimilar = true; /* ── Styles ── */ @@ -370,6 +374,70 @@ export class ExploreArtistDetails extends LitElement { color: var(--yj-text-tertiary, #888); font-size: var(--yj-text-xs); } + + /* ── Similar artists ── */ + .horizontal-row { + display: flex; + gap: 12px; + overflow-x: auto; + padding-bottom: 4px; + scrollbar-width: none; + } + + .horizontal-row::-webkit-scrollbar { + display: none; + } + + .similar-artist-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 10px; + border-radius: 8px; + cursor: pointer; + min-width: 100px; + max-width: 120px; + flex-shrink: 0; + text-align: center; + transition: background 0.15s ease; + } + + .similar-artist-card:hover { + background: var( + --yj-bg-overlay, + rgba(255, 255, 255, 0.06) + ); + } + + .similar-artist-card:active { + transform: scale(0.97); + } + + .similar-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-weight: 600; + font-size: 20px; + text-transform: uppercase; + user-select: none; + flex-shrink: 0; + } + + .similar-name { + font-weight: 500; + color: var(--yj-text-primary, #fff); + font-size: var(--yj-text-sm); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + } `, ]; @@ -387,23 +455,27 @@ export class ExploreArtistDetails extends LitElement { private async loadAllData() { const mbid = this.artistMBID; console.log( - `[explore-artist] loading artist page: "${this.artistName}" (${mbid})`, + `[explore-artist] loading: "${this.artistName}" (${mbid})`, ); - // Fire all three requests in parallel — each section is independent. - const [artistResult, tracksResult, releasesResult] = + // Fire all four requests in parallel — each section is independent. + const [artistResult, tracksResult, releasesResult, similarResult] = await Promise.allSettled([ this.fetchArtist(mbid), this.fetchTopTracks(mbid), this.fetchReleaseGroups(mbid), + this.fetchSimilarArtists(mbid), ]); const summary = [ `artist=${artistResult.status}`, `tracks=${tracksResult.status}`, `releases=${releasesResult.status}`, + `similar=${similarResult.status}`, ].join(', '); - console.log(`[explore-artist] load complete: ${summary}`); + console.log( + `[explore-artist] loaded: "${this.artistName}" (${summary})`, + ); } private async fetchArtist(mbid: string) { @@ -412,7 +484,7 @@ export class ExploreArtistDetails extends LitElement { } catch (err) { const msg = err instanceof Error ? err.message : String(err); this.errorArtist = msg; - console.error(`[explore-artist] LookupArtist failed: ${msg}`); + console.error(`[explore-artist] LookupArtist error: ${msg}`); } finally { this.loadingArtist = false; } @@ -426,7 +498,7 @@ export class ExploreArtistDetails extends LitElement { const msg = err instanceof Error ? err.message : String(err); this.errorTracks = msg; console.error( - `[explore-artist] TopRecordingsForArtist failed: ${msg}`, + `[explore-artist] TopRecordingsForArtist error: ${msg}`, ); } finally { this.loadingTracks = false; @@ -441,13 +513,29 @@ export class ExploreArtistDetails extends LitElement { const msg = err instanceof Error ? err.message : String(err); this.errorReleases = msg; console.error( - `[explore-artist] BrowseReleaseGroups failed: ${msg}`, + `[explore-artist] BrowseReleaseGroups error: ${msg}`, ); } finally { this.loadingReleases = false; } } + private async fetchSimilarArtists(mbid: string) { + try { + const artists = await SimilarArtists(mbid); + this.similarArtists = artists ?? []; + } catch (err) { + // D024: graceful degradation — silently omit similar artists on failure. + const msg = err instanceof Error ? err.message : String(err); + console.error( + `[explore-artist] SimilarArtists error: ${msg}`, + ); + this.similarArtists = []; + } finally { + this.loadingSimilar = false; + } + } + /* ── Navigation ── */ private navigateBack() { @@ -474,6 +562,20 @@ export class ExploreArtistDetails extends LitElement { ); } + private navigateToSimilarArtist(artist: LBSimilarArtist) { + this.dispatchEvent( + new CustomEvent('navigate', { + bubbles: true, + composed: true, + detail: { + view: 'explore-artist-details', + artistMBID: artist.artistMbid, + artistName: artist.name, + }, + }), + ); + } + /* ── Image Error Handling ── */ private handleImageError(e: Event) { @@ -574,7 +676,7 @@ export class ExploreArtistDetails extends LitElement {