Add discography filters to artist detail page

Introduce interactive discography filters on the artist detail page. Adds filter UI markup (category, content, ownership) in index.html and styles in style.css. Implements filter state, initialization, reset and apply logic in script.js; tags release cards with data attributes (live/compilation/featured) using regex heuristics, updates visibility and per-section owned/missing counts, and re-applies filters when releases are updated. Integrates filter setup into page init and resets filters when loading artist data.
This commit is contained in:
Broque Thomas 2026-02-26 10:17:53 -08:00
parent fff8e62070
commit 44b5032f17
3 changed files with 230 additions and 0 deletions

View file

@ -2061,6 +2061,30 @@
<!-- Main Content -->
<div class="artist-detail-main" id="artist-detail-main">
<!-- Discography Filters -->
<div class="discography-filters" id="discography-filters">
<div class="filter-group">
<span class="filter-label">Show</span>
<button class="discography-filter-btn active" data-filter="category" data-value="albums">Albums</button>
<button class="discography-filter-btn active" data-filter="category" data-value="eps">EPs</button>
<button class="discography-filter-btn active" data-filter="category" data-value="singles">Singles</button>
</div>
<div class="filter-divider"></div>
<div class="filter-group">
<span class="filter-label">Include</span>
<button class="discography-filter-btn active" data-filter="content" data-value="live">Live</button>
<button class="discography-filter-btn active" data-filter="content" data-value="compilations">Compilations</button>
<button class="discography-filter-btn active" data-filter="content" data-value="featured">Featured</button>
</div>
<div class="filter-divider"></div>
<div class="filter-group">
<span class="filter-label">Status</span>
<button class="discography-filter-btn active" data-filter="ownership" data-value="all">All</button>
<button class="discography-filter-btn" data-filter="ownership" data-value="owned">Owned</button>
<button class="discography-filter-btn" data-filter="ownership" data-value="missing">Missing</button>
</div>
</div>
<!-- Discography Sections -->
<div class="discography-sections">
<!-- Albums Section -->

View file

@ -27085,6 +27085,13 @@ let artistDetailPageState = {
currentArtistName: null
};
// Discography filter state
let discographyFilterState = {
categories: { albums: true, eps: true, singles: true },
content: { live: true, compilations: true, featured: true },
ownership: 'all' // 'all', 'owned', 'missing'
};
function navigateToArtistDetail(artistId, artistName) {
console.log(`🎵 Navigating to artist detail: ${artistName} (ID: ${artistId})`);
@ -27140,6 +27147,9 @@ function initializeArtistDetailPage() {
});
}
// Initialize discography filter buttons
initializeDiscographyFilters();
artistDetailPageState.isInitialized = true;
console.log("✅ Artist Detail page initialized successfully");
}
@ -27147,6 +27157,9 @@ function initializeArtistDetailPage() {
async function loadArtistDetailData(artistId, artistName) {
console.log(`🔄 Loading artist detail data for: ${artistName} (ID: ${artistId})`);
// Reset discography filters to defaults
resetDiscographyFilters();
// Show loading state and hide all content
showArtistDetailLoading(true);
showArtistDetailError(false);
@ -27491,6 +27504,9 @@ function populateDiscographySections(discography) {
// Populate singles
populateReleaseSection('singles', discography.singles);
// Apply any active filters after populating
applyDiscographyFilters();
}
function populateReleaseSection(sectionType, releases) {
@ -27540,6 +27556,18 @@ function createReleaseCard(release) {
// 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;
const isLive = livePattern.test(release.title || '') || (release.album_type === 'compilation' && livePattern.test(release.title || ''));
const isCompilation = (release.album_type === 'compilation') || compilationPattern.test(release.title || '');
const isFeatured = featuredPattern.test(release.title || '');
card.setAttribute("data-is-live", isLive ? "true" : "false");
card.setAttribute("data-is-compilation", isCompilation ? "true" : "false");
card.setAttribute("data-is-featured", isFeatured ? "true" : "false");
// Add MusicBrainz icon if available
let mbIcon = null;
if (release.musicbrainz_release_id) {
@ -27969,6 +27997,9 @@ function updateLibraryReleaseCard(data) {
formatsDiv.innerHTML = data.formats.map(f => `<span class="release-format-tag">${f}</span>`).join('');
card.appendChild(formatsDiv);
}
// Re-apply filters so newly resolved cards respect active filters
applyDiscographyFilters();
}
function updateCategoryStatsFromStream(category, ownedCount, missingCount) {
@ -28066,6 +28097,125 @@ function recalculateSummaryStats(artistFormatSet) {
}
}
// ===============================================
// Discography Filter Functions
// ===============================================
function initializeDiscographyFilters() {
const container = document.getElementById('discography-filters');
if (!container) return;
container.addEventListener('click', (e) => {
const btn = e.target.closest('.discography-filter-btn');
if (!btn) return;
const filterType = btn.dataset.filter;
const value = btn.dataset.value;
if (filterType === 'category') {
// Multi-toggle: toggle this category on/off
btn.classList.toggle('active');
discographyFilterState.categories[value] = btn.classList.contains('active');
} else if (filterType === 'content') {
// Multi-toggle: toggle this content type on/off
btn.classList.toggle('active');
discographyFilterState.content[value] = btn.classList.contains('active');
} else if (filterType === 'ownership') {
// Single-select: deactivate siblings, activate this one
container.querySelectorAll('[data-filter="ownership"]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
discographyFilterState.ownership = value;
}
applyDiscographyFilters();
});
}
function resetDiscographyFilters() {
discographyFilterState.categories = { albums: true, eps: true, singles: true };
discographyFilterState.content = { live: true, compilations: true, featured: true };
discographyFilterState.ownership = 'all';
// Reset button visual states
const container = document.getElementById('discography-filters');
if (!container) return;
container.querySelectorAll('.discography-filter-btn').forEach(btn => {
const filterType = btn.dataset.filter;
const value = btn.dataset.value;
if (filterType === 'ownership') {
btn.classList.toggle('active', value === 'all');
} else {
btn.classList.add('active');
}
});
}
function applyDiscographyFilters() {
const categories = ['albums', 'eps', 'singles'];
for (const cat of categories) {
const section = document.getElementById(`${cat}-section`);
if (!section) continue;
// Category toggle — hide entire section
if (!discographyFilterState.categories[cat]) {
section.style.display = 'none';
continue;
}
section.style.display = '';
// Filter individual cards within the section
const grid = document.getElementById(`${cat}-grid`);
if (!grid) continue;
let visibleOwned = 0;
let visibleMissing = 0;
let visibleCount = 0;
grid.querySelectorAll('.release-card').forEach(card => {
let hidden = false;
// Content filters
if (!discographyFilterState.content.live && card.getAttribute('data-is-live') === 'true') {
hidden = true;
}
if (!discographyFilterState.content.compilations && card.getAttribute('data-is-compilation') === 'true') {
hidden = true;
}
if (!discographyFilterState.content.featured && card.getAttribute('data-is-featured') === 'true') {
hidden = true;
}
// Ownership filter (only apply if card is not still checking)
if (!hidden && discographyFilterState.ownership !== 'all' && card._releaseData) {
const owned = card._releaseData.owned;
if (owned !== null) { // Don't hide cards still being checked
if (discographyFilterState.ownership === 'owned' && !owned) hidden = true;
if (discographyFilterState.ownership === 'missing' && owned) hidden = true;
}
}
card.style.display = hidden ? 'none' : '';
// Count visible cards for stats
if (!hidden && card._releaseData) {
visibleCount++;
if (card._releaseData.owned === true) visibleOwned++;
else if (card._releaseData.owned === false) visibleMissing++;
}
});
// Update section stats to reflect filtered view
const ownedEl = document.getElementById(`${cat}-owned-count`);
const missingEl = document.getElementById(`${cat}-missing-count`);
if (ownedEl) ownedEl.textContent = `${visibleOwned} owned`;
if (missingEl) missingEl.textContent = `${visibleMissing} missing`;
// Hide section entirely if all cards are hidden
section.style.display = visibleCount === 0 ? 'none' : '';
}
}
// UI state management functions
function showArtistDetailLoading(show) {
const loadingElement = document.getElementById("artist-detail-loading");

View file

@ -13566,6 +13566,62 @@ body {
letter-spacing: 0.5px;
}
/* Discography Filters */
.discography-filters {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0 20px;
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 6px;
}
.filter-label {
font-size: 11px;
font-weight: 600;
color: rgba(255, 255, 255, 0.35);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-right: 4px;
}
.filter-divider {
width: 1px;
height: 24px;
background: rgba(255, 255, 255, 0.1);
margin: 0 4px;
}
.discography-filter-btn {
padding: 5px 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.4);
font-size: 12px;
font-weight: 600;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif;
cursor: pointer;
border-radius: 16px;
transition: all 0.2s ease-out;
}
.discography-filter-btn:hover {
color: rgba(255, 255, 255, 0.8);
border-color: rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.08);
}
.discography-filter-btn.active {
color: #fff;
border-color: rgba(29, 185, 84, 0.5);
background: rgba(29, 185, 84, 0.15);
}
/* Discography Sections */
.discography-sections {
display: flex;