discover: convert the two artist sections to the .ya-card grid

Based On Your Listening + Recommended For You now use the same .ya-card as the rest (square cover,
name overlaid, recommendation reason as the subtitle) in wrapping grids with the Show-all clamp,
replacing the bespoke recommended-artist-card layout. The watchlist toggle and live image enrichment
are preserved by keeping their hooks on the new markup — .recommended-artist-card[data-artist-id],
.recommended-card-image, and .recommended-card-watchlist-btn (restyled as a hover-reveal corner pill
that stays lit when watching). _clampGrid wired into both onRendered callbacks.

64 script-integrity tests green.
This commit is contained in:
BoulderBadgeDad 2026-06-29 14:32:16 -07:00
parent 4cab44889c
commit bf8e8beeb7
3 changed files with 37 additions and 16 deletions

View file

@ -3155,7 +3155,7 @@
<p class="discover-section-subtitle">Artists you'd love — ranked from who you actually play the most</p>
</div>
</div>
<div class="discover-carousel" id="listening-recs-carousel">
<div class="discover-grid" id="listening-recs-carousel">
<!-- Populated by JS -->
</div>
</div>
@ -3219,7 +3219,7 @@
</button>
</div>
</div>
<div class="discover-carousel" id="recommended-artists-carousel">
<div class="discover-grid" id="recommended-artists-carousel">
<!-- Populated by JS -->
</div>
</div>

View file

@ -701,32 +701,32 @@ function _renderRecommendedMini(artist, source, opts) {
const artistSource = artist.source || source || '';
const reason = reasonFn(artist);
const reasonTitle = titleFn(artist);
const genreTags = (artist.genres || []).slice(0, 2).map(g =>
`<span class="recommended-card-genre">${escapeHtml(g)}</span>`
).join('');
const img = artist.image_url
? `<img src="${artist.image_url}" alt="${escapeHtml(artist.artist_name)}" loading="lazy"
onerror="this.parentElement.innerHTML='<div class=\\'recommended-card-image-fallback\\'>🎤</div>';">`
: `<div class="recommended-card-image-fallback">🎤</div>`;
// .ya-card visual (matches Your Artists / albums) while keeping the class + data hooks the
// watchlist handler (.recommended-card-watchlist-btn) and image enrichment
// (.recommended-artist-card[data-artist-id] → .recommended-card-image) rely on. #discover redesign.
return `
<div class="recommended-artist-card recommended-card--carousel"
<div class="ya-card recommended-artist-card"
data-artist-name="${escapeHtml(artist.artist_name).toLowerCase()}"
data-artist-id="${artist.artist_id}"
data-artist-source="${escapeHtml(artistSource)}">
<button class="recommended-card-watchlist-btn"
<a class="recommended-card-link" href="${buildArtistDetailPath(artist.artist_id, artistSource || null)}"
style="display:block;text-decoration:none;color:inherit;">
<div class="ya-card-img recommended-card-image">${img}</div>
<div class="ya-card-gradient"></div>
<div class="ya-card-info">
<div class="ya-card-name">${escapeHtml(artist.artist_name)}</div>
<div class="ya-card-sub" title="${escapeHtml(reasonTitle)}">${reason}</div>
</div>
</a>
<button class="recommended-card-watchlist-btn ya-card-reco-btn"
data-artist-id="${artist.artist_id}"
data-artist-name="${escapeHtml(artist.artist_name)}">
Add to Watchlist
</button>
<a class="recommended-card-link" href="${buildArtistDetailPath(artist.artist_id, artistSource || null)}"
style="display:block;text-decoration:none;color:inherit;">
<div class="recommended-card-image">${img}</div>
<div class="recommended-card-info">
<span class="recommended-card-name">${escapeHtml(artist.artist_name)}</span>
<span class="recommended-card-similarity" title="${escapeHtml(reasonTitle)}">${reason}</span>
<div class="recommended-card-genres">${genreTags}</div>
</div>
</a>
</div>`;
}
@ -790,6 +790,7 @@ async function loadRecommendedArtistsSection() {
}
const source = (data && data.source) || 'spotify';
_enrichRecommendedCarouselCards((data && data.artists || []).slice(0, 18), source);
_clampGrid(carousel);
},
loadingMessage: 'Finding recommendations...',
emptyMessage: 'No recommendations yet — let the Similar Artists worker run',
@ -832,6 +833,7 @@ async function loadListeningRecommendations() {
}
const source = (data && data.source) || 'spotify';
_enrichRecommendedCarouselCards((data && data.artists || []).slice(0, 18), source, 'listening-recs-carousel');
_clampGrid(carousel);
},
loadingMessage: 'Reading your listening...',
emptyMessage: 'Play more music and run a watchlist scan to see picks based on your listening',

View file

@ -35144,6 +35144,25 @@ div.artist-hero-badge {
border-color: rgba(var(--accent-rgb),0.4);
color: #fff;
}
/* Watchlist toggle on recommended-artist .ya-cards corner pill, appears on hover,
stays lit when already watching. Keeps the .recommended-card-watchlist-btn hook. */
.recommended-artist-card .recommended-card-watchlist-btn {
position: absolute; top: 8px; right: 8px; z-index: 3;
background: rgba(0,0,0,0.72); color: #fff;
border: 1px solid rgba(255,255,255,0.25);
border-radius: 14px; padding: 4px 10px;
font-size: 10px; font-weight: 600; cursor: pointer;
opacity: 0; transform: translateY(-4px);
transition: opacity 0.2s, transform 0.2s, background 0.2s, border-color 0.2s;
}
.recommended-artist-card:hover .recommended-card-watchlist-btn { opacity: 1; transform: none; }
.recommended-artist-card .recommended-card-watchlist-btn:hover {
background: var(--accent); color: #000; border-color: transparent;
}
.recommended-artist-card .recommended-card-watchlist-btn.watching {
opacity: 1; transform: none;
background: var(--accent); color: #000; border-color: transparent;
}
/* ── Artist Info Modal ── */
.ya-info-modal {