Restore big-photo album cards on artist-detail page
The standalone /artist-detail page rendered releases via createReleaseCard
in a stacked layout: square image on top, then title, then year, then a
completion bar — all inside a 300px-tall card with internal padding. The
inline Artists page (now retired) used a richer treatment: full-bleed
artwork with a dark gradient overlay and the title + year pinned at the
bottom. This commit brings that look to the standalone page.
Card markup (still .release-card so all the existing JS filter +
state hooks work, plus .album-card for the visual):
<div class="release-card album-card" ...>
<div class="album-card-image" data-bg-src="..."></div>
<div class="completion-overlay [state]">
<span class="completion-status">...</span>
</div>
<div class="album-card-content">
<div class="album-card-name">title</div>
<div class="album-card-year">year</div>
</div>
[optional .mb-card-icon]
</div>
Image loads lazily via the existing observeLazyBackgrounds /
data-bg-src plumbing in core.js — call moved into populateRelease-
Section so each batch of new cards gets observed.
Completion overlay (top-right floating badge):
- Library artists: 'Checking…' / '✓ Owned' / 'N/M' / 'X%' / 'Missing'
based on release.owned + track_completion shape (existing logic
preserved, just rendered as a badge instead of a bar).
- Source artists (no library data): omitted entirely. The card just
shows artwork + title + year, which is what the user asked for.
CSS: scoped overrides under #artist-detail-page .release-card.album-card
neutralize the old release-card background gradient, internal padding,
fixed 300px height, and flex column layout. Cards become aspect-ratio:1
square with overflow:hidden so the image fills and the gradient + text
sit on top.
Filter state (data-is-live / data-is-compilation / data-is-featured)
still tagged on each card so the Include filter group keeps working.
Smoke: library Kendrick Lamar should now look like the inline Artists
page used to — square cards, big artwork, name + year on the bottom.
Source-clicked artist (Schoolboy Q from Deezer) shows the same
visual without the completion overlay.
This commit is contained in:
parent
9b52c1d94f
commit
59298b5b17
2 changed files with 163 additions and 150 deletions
|
|
@ -1391,22 +1391,34 @@ function populateReleaseSection(sectionType, releases) {
|
|||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
// Trigger lazy background-image loading on the new cards
|
||||
if (typeof observeLazyBackgrounds === 'function') {
|
||||
observeLazyBackgrounds(grid);
|
||||
}
|
||||
|
||||
console.log(`📀 Populated ${sectionType} section: ${ownedCount} owned, ${missingCount} missing`);
|
||||
console.log(`📀 Grid element:`, grid);
|
||||
console.log(`📀 Grid children count:`, grid.children.length);
|
||||
}
|
||||
|
||||
function createReleaseCard(release) {
|
||||
const card = document.createElement("div");
|
||||
const isChecking = release.owned === null;
|
||||
card.className = `release-card${isChecking ? " checking" : (release.owned ? "" : " missing")}`;
|
||||
// .release-card keeps existing filter/state CSS + JS queries working;
|
||||
// .album-card adopts the big-photo visual treatment from the retired
|
||||
// inline Artists page (full-bleed image, gradient overlay, info pinned).
|
||||
let stateCls = '';
|
||||
if (isChecking) stateCls = ' checking';
|
||||
else if (release.owned === false) stateCls = ' missing';
|
||||
card.className = `release-card album-card${stateCls}`;
|
||||
|
||||
const releaseId = release.id || "";
|
||||
card.setAttribute("data-release-id", releaseId);
|
||||
card.setAttribute("data-album-id", releaseId);
|
||||
card.setAttribute("data-album-name", release.title || "");
|
||||
card.setAttribute("data-album-type", release.album_type || "album");
|
||||
// Store mutable reference so stream updates propagate to click handler
|
||||
card._releaseData = release;
|
||||
|
||||
// Tag card for content-type filtering
|
||||
const titleLower = (release.title || '').toLowerCase();
|
||||
const livePattern = /\b(live)\b|\(live[^)]*\)|\[live[^]]*\]/i;
|
||||
const compilationPattern = /\b(greatest hits|best of|collection|anthology|essential)\b/i;
|
||||
const featuredPattern = /\(?\bfeat\.?\s|\bft\.?\s|\bfeaturing\b/i;
|
||||
|
|
@ -1417,10 +1429,90 @@ function createReleaseCard(release) {
|
|||
card.setAttribute("data-is-compilation", isCompilation ? "true" : "false");
|
||||
card.setAttribute("data-is-featured", isFeatured ? "true" : "false");
|
||||
|
||||
// Add MusicBrainz icon if available
|
||||
let mbIcon = null;
|
||||
// Background image — use data-bg-src for IntersectionObserver lazy loading
|
||||
// (observeLazyBackgrounds is called by the caller after appending the grid).
|
||||
const imageDiv = document.createElement("div");
|
||||
imageDiv.className = "album-card-image";
|
||||
if (release.image_url && release.image_url.trim() !== "") {
|
||||
imageDiv.dataset.bgSrc = release.image_url;
|
||||
}
|
||||
card.appendChild(imageDiv);
|
||||
|
||||
// Completion overlay — top-right floating badge. For library artists this
|
||||
// shows the ownership state; for source artists (no library data) the
|
||||
// overlay is omitted entirely so the card just shows the artwork + title.
|
||||
const isSourceContext = (document.body.dataset.artistSource === 'source');
|
||||
if (!isSourceContext) {
|
||||
const overlay = document.createElement("div");
|
||||
let overlayCls = '';
|
||||
let overlayLabel = '';
|
||||
|
||||
if (isChecking || release.track_completion === 'checking') {
|
||||
overlayCls = 'checking';
|
||||
overlayLabel = 'Checking...';
|
||||
} else if (release.owned) {
|
||||
const tc = release.track_completion;
|
||||
if (tc && typeof tc === 'object') {
|
||||
const ownedTracks = tc.owned_tracks || 0;
|
||||
const totalTracks = tc.total_tracks || 0;
|
||||
const missingTracks = tc.missing_tracks || 0;
|
||||
if (missingTracks === 0) {
|
||||
overlayCls = 'completed';
|
||||
overlayLabel = '✓ Owned';
|
||||
} else {
|
||||
const pct = totalTracks > 0 ? Math.round((ownedTracks / totalTracks) * 100) : 0;
|
||||
overlayCls = pct >= 75 ? 'nearly_complete' : 'partial';
|
||||
overlayLabel = `${ownedTracks}/${totalTracks}`;
|
||||
}
|
||||
} else {
|
||||
const pct = release.track_completion || 100;
|
||||
if (pct === 100) {
|
||||
overlayCls = 'completed';
|
||||
overlayLabel = '✓ Owned';
|
||||
} else {
|
||||
overlayCls = pct >= 75 ? 'nearly_complete' : 'partial';
|
||||
overlayLabel = `${pct}%`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
overlayCls = 'missing';
|
||||
overlayLabel = 'Missing';
|
||||
}
|
||||
|
||||
overlay.className = `completion-overlay ${overlayCls}`;
|
||||
overlay.innerHTML = `<span class="completion-status">${overlayLabel}</span>`;
|
||||
card.appendChild(overlay);
|
||||
}
|
||||
|
||||
// Year — extract from release_date or fall back to year field
|
||||
let yearText = "";
|
||||
if (release.release_date) {
|
||||
try {
|
||||
const yearMatch = release.release_date.match(/^(\d{4})/);
|
||||
if (yearMatch) {
|
||||
const ry = parseInt(yearMatch[1]);
|
||||
if (ry && ry > 1900 && ry <= new Date().getFullYear() + 1) yearText = ry.toString();
|
||||
} else {
|
||||
const ry = new Date(release.release_date).getFullYear();
|
||||
if (ry && !isNaN(ry) && ry > 1900 && ry <= new Date().getFullYear() + 1) yearText = ry.toString();
|
||||
}
|
||||
} catch (e) { /* fall through */ }
|
||||
}
|
||||
if (!yearText && release.year) yearText = release.year.toString();
|
||||
|
||||
// Content (bottom-pinned over gradient)
|
||||
const content = document.createElement("div");
|
||||
content.className = "album-card-content";
|
||||
const _esc = (s) => String(s || '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
content.innerHTML = `
|
||||
<div class="album-card-name" title="${_esc(release.title)}">${_esc(release.title)}</div>
|
||||
${yearText ? `<div class="album-card-year">${_esc(yearText)}</div>` : ''}
|
||||
`;
|
||||
card.appendChild(content);
|
||||
|
||||
// Add MusicBrainz icon LAST so it sits above the gradient overlay
|
||||
if (release.musicbrainz_release_id) {
|
||||
mbIcon = document.createElement("div");
|
||||
const mbIcon = document.createElement("div");
|
||||
mbIcon.className = "mb-card-icon";
|
||||
mbIcon.title = "View on MusicBrainz";
|
||||
mbIcon.innerHTML = `<img src="${MUSICBRAINZ_LOGO_URL}" style="width: 20px; height: auto; display: block;">`;
|
||||
|
|
@ -1428,149 +1520,6 @@ function createReleaseCard(release) {
|
|||
e.stopPropagation();
|
||||
window.open(`https://musicbrainz.org/release/${release.musicbrainz_release_id}`, '_blank');
|
||||
};
|
||||
}
|
||||
|
||||
// Create image
|
||||
const imageContainer = document.createElement("div");
|
||||
if (release.image_url && release.image_url.trim() !== "") {
|
||||
const img = document.createElement("img");
|
||||
img.src = release.image_url;
|
||||
img.alt = release.title;
|
||||
img.className = "release-image";
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => {
|
||||
imageContainer.innerHTML = `<div class="release-image-fallback">💿</div>`;
|
||||
};
|
||||
imageContainer.appendChild(img);
|
||||
} else {
|
||||
imageContainer.innerHTML = `<div class="release-image-fallback">💿</div>`;
|
||||
}
|
||||
|
||||
// Create title
|
||||
const title = document.createElement("h4");
|
||||
title.className = "release-title";
|
||||
title.textContent = release.title;
|
||||
title.title = release.title;
|
||||
|
||||
// Create year - extract from release_date (Spotify format) or fall back to year field
|
||||
const year = document.createElement("div");
|
||||
year.className = "release-year";
|
||||
|
||||
let yearText = "Unknown Year";
|
||||
|
||||
// DEBUG: Log the release data to see what we're working with (remove this after testing)
|
||||
// console.log(`🔍 DEBUG: Release "${release.title}" data:`, {
|
||||
// title: release.title,
|
||||
// owned: release.owned,
|
||||
// year: release.year,
|
||||
// release_date: release.release_date,
|
||||
// track_completion: release.track_completion
|
||||
// });
|
||||
|
||||
// First try to extract year from release_date (Spotify format: "YYYY-MM-DD")
|
||||
if (release.release_date) {
|
||||
try {
|
||||
// Extract year directly from string to avoid timezone issues
|
||||
const yearMatch = release.release_date.match(/^(\d{4})/);
|
||||
if (yearMatch) {
|
||||
const releaseYear = parseInt(yearMatch[1]);
|
||||
if (releaseYear && !isNaN(releaseYear) && releaseYear > 1900 && releaseYear <= new Date().getFullYear() + 1) {
|
||||
yearText = releaseYear.toString();
|
||||
}
|
||||
} else {
|
||||
// Fallback to Date parsing if format is different
|
||||
const releaseYear = new Date(release.release_date).getFullYear();
|
||||
if (releaseYear && !isNaN(releaseYear) && releaseYear > 1900 && releaseYear <= new Date().getFullYear() + 1) {
|
||||
yearText = releaseYear.toString();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error parsing release_date:', release.release_date, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to direct year field if release_date parsing failed
|
||||
if (yearText === "Unknown Year" && release.year) {
|
||||
yearText = release.year.toString();
|
||||
}
|
||||
|
||||
year.textContent = yearText;
|
||||
|
||||
// Create completion info
|
||||
const completion = document.createElement("div");
|
||||
completion.className = "release-completion";
|
||||
|
||||
const completionText = document.createElement("span");
|
||||
const completionBar = document.createElement("div");
|
||||
completionBar.className = "completion-bar";
|
||||
|
||||
const completionFill = document.createElement("div");
|
||||
completionFill.className = "completion-fill";
|
||||
|
||||
if (release.owned === null || release.track_completion === 'checking') {
|
||||
// Checking state - ownership not yet resolved
|
||||
completionText.textContent = "Checking...";
|
||||
completionText.className = "completion-text checking";
|
||||
completionFill.className += " checking";
|
||||
completionFill.style.width = "100%";
|
||||
} else if (release.owned) {
|
||||
// Handle new detailed track completion object
|
||||
if (release.track_completion && typeof release.track_completion === 'object') {
|
||||
const completion = release.track_completion;
|
||||
const percentage = completion.percentage || 100;
|
||||
const ownedTracks = completion.owned_tracks || 0;
|
||||
const totalTracks = completion.total_tracks || 0;
|
||||
const missingTracks = completion.missing_tracks || 0;
|
||||
|
||||
completionFill.style.width = `${percentage}%`;
|
||||
|
||||
if (missingTracks === 0) {
|
||||
completionText.textContent = `Complete (${ownedTracks})`;
|
||||
completionText.className = "completion-text complete";
|
||||
completionFill.className += " complete";
|
||||
} else {
|
||||
completionText.textContent = `${ownedTracks}/${totalTracks} tracks`;
|
||||
completionText.className = "completion-text partial";
|
||||
completionFill.className += " partial";
|
||||
|
||||
// Add missing tracks indicator
|
||||
completionText.title = `Missing ${missingTracks} track${missingTracks !== 1 ? 's' : ''}`;
|
||||
}
|
||||
} else {
|
||||
// Fallback for legacy simple percentage
|
||||
const percentage = release.track_completion || 100;
|
||||
completionFill.style.width = `${percentage}%`;
|
||||
|
||||
if (percentage === 100) {
|
||||
completionText.textContent = "Complete";
|
||||
completionText.className = "completion-text complete";
|
||||
completionFill.className += " complete";
|
||||
} else {
|
||||
completionText.textContent = `${percentage}%`;
|
||||
completionText.className = "completion-text partial";
|
||||
completionFill.className += " partial";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const totalTr = release.total_tracks || release.track_completion?.total_tracks || 0;
|
||||
completionText.textContent = totalTr > 0 ? `Missing (${totalTr} tracks)` : "Not in library";
|
||||
completionText.className = "completion-text missing";
|
||||
completionFill.className += " missing";
|
||||
completionFill.style.width = "0%";
|
||||
}
|
||||
|
||||
completionBar.appendChild(completionFill);
|
||||
completion.appendChild(completionText);
|
||||
completion.appendChild(completionBar);
|
||||
|
||||
// Assemble card
|
||||
card.appendChild(imageContainer);
|
||||
card.appendChild(title);
|
||||
card.appendChild(year);
|
||||
card.appendChild(completion);
|
||||
|
||||
// Add MusicBrainz icon LAST to ensure it's on top
|
||||
if (release.musicbrainz_release_id && mbIcon) { // Check if mbIcon was created
|
||||
card.appendChild(mbIcon);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59449,3 +59449,67 @@ body[data-artist-source="source"] #artist-detail-page #library-artist-enhance-bt
|
|||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
||||
/* =========================================================================
|
||||
Album cards on /artist-detail — big-photo treatment
|
||||
=========================================================================
|
||||
.release-card keeps the existing JS filter/state hooks; .album-card
|
||||
layers on the visual treatment from the retired inline Artists page
|
||||
(full-bleed image, gradient overlay, info pinned at bottom).
|
||||
|
||||
The base .release-card rule still applies (background gradient, padding,
|
||||
fixed 300px height, flex column). These overrides switch off the parts
|
||||
that fight with the .album-card layout. */
|
||||
|
||||
#artist-detail-page .release-card.album-card {
|
||||
background: rgba(18, 18, 18, 1);
|
||||
backdrop-filter: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 14px;
|
||||
padding: 0;
|
||||
display: block;
|
||||
height: auto;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
#artist-detail-page .release-card.album-card:hover {
|
||||
background: rgba(18, 18, 18, 1);
|
||||
transform: translateY(-5px) scale(1.02);
|
||||
border-color: rgba(var(--accent-rgb), 0.25);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5),
|
||||
0 0 24px rgba(var(--accent-rgb), 0.12);
|
||||
}
|
||||
|
||||
#artist-detail-page .release-card.album-card.missing {
|
||||
opacity: 1; /* let the completion-overlay convey state, not the whole card */
|
||||
}
|
||||
|
||||
/* The fit-content override on the discography page also conflicts with
|
||||
aspect-ratio:1 — clear it for album-card variants. */
|
||||
#artist-detail-page .discography-sections .release-card.album-card {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* MusicBrainz icon position (was tied to the old card's padding) */
|
||||
#artist-detail-page .release-card.album-card .mb-card-icon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 3;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
border-radius: 6px;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
opacity: 0.85;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
#artist-detail-page .release-card.album-card .mb-card-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue