library page update
This commit is contained in:
parent
9485dfb03b
commit
03b0b8c6c5
4 changed files with 1583 additions and 9 deletions
301
web_server.py
301
web_server.py
|
|
@ -2687,6 +2687,96 @@ def get_library_artists():
|
|||
}
|
||||
}), 500
|
||||
|
||||
@app.route('/api/test-artist/<artist_id>')
|
||||
def test_artist_endpoint(artist_id):
|
||||
"""Simple test endpoint"""
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"Test endpoint working for artist ID: {artist_id}"
|
||||
})
|
||||
|
||||
@app.route('/api/artist-detail/<artist_id>')
|
||||
def get_artist_detail(artist_id):
|
||||
"""Get artist detail data"""
|
||||
try:
|
||||
print(f"🎵 Getting artist detail for ID: {artist_id}")
|
||||
|
||||
# Get database instance
|
||||
database = get_database()
|
||||
|
||||
# Get artist discography from database
|
||||
db_result = database.get_artist_discography(artist_id)
|
||||
|
||||
if not db_result.get('success'):
|
||||
print(f"❌ Database returned error: {db_result}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": db_result.get('error', 'Artist not found')
|
||||
}), 404
|
||||
|
||||
artist_info = db_result['artist']
|
||||
owned_releases = db_result['owned_releases']
|
||||
|
||||
print(f"✅ Found artist: {artist_info['name']} with {len(owned_releases['albums'])} albums")
|
||||
|
||||
# Fix artist image URL
|
||||
print(f"🖼️ Artist image before fix: '{artist_info.get('image_url')}'")
|
||||
if artist_info.get('image_url'):
|
||||
artist_info['image_url'] = fix_artist_image_url(artist_info['image_url'])
|
||||
print(f"🖼️ Artist image after fix: '{artist_info['image_url']}'")
|
||||
else:
|
||||
print(f"🖼️ No artist image URL found for {artist_info['name']}")
|
||||
|
||||
# Debug final artist data being sent
|
||||
print(f"🖼️ Final artist data being sent: {artist_info}")
|
||||
|
||||
# Fix image URLs for all albums
|
||||
for album in owned_releases['albums']:
|
||||
if album.get('image_url'):
|
||||
album['image_url'] = fix_artist_image_url(album['image_url'])
|
||||
|
||||
# Fix image URLs for EPs and singles (currently empty but for future use)
|
||||
for ep in owned_releases['eps']:
|
||||
if ep.get('image_url'):
|
||||
ep['image_url'] = fix_artist_image_url(ep['image_url'])
|
||||
|
||||
for single in owned_releases['singles']:
|
||||
if single.get('image_url'):
|
||||
single['image_url'] = fix_artist_image_url(single['image_url'])
|
||||
|
||||
# Get Spotify discography for proper categorization and missing releases
|
||||
try:
|
||||
spotify_discography = get_spotify_artist_discography(artist_info['name'])
|
||||
|
||||
if spotify_discography['success']:
|
||||
print(f"🎵 Spotify discography found - Albums: {len(spotify_discography['albums'])}, EPs: {len(spotify_discography['eps'])}, Singles: {len(spotify_discography['singles'])}")
|
||||
|
||||
# Merge owned and Spotify data for complete picture
|
||||
merged_discography = merge_discography_data(owned_releases, spotify_discography)
|
||||
else:
|
||||
print(f"⚠️ Spotify discography not found: {spotify_discography.get('error', 'Unknown error')}")
|
||||
# Fall back to our database categorization
|
||||
merged_discography = owned_releases
|
||||
except Exception as spotify_error:
|
||||
print(f"⚠️ Error fetching Spotify data: {spotify_error}")
|
||||
# Fall back to our database categorization
|
||||
merged_discography = owned_releases
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"artist": artist_info,
|
||||
"discography": merged_discography
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error in get_artist_detail: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/library/debug-photos')
|
||||
def debug_library_photos():
|
||||
"""Debug endpoint to check artist photo URLs"""
|
||||
|
|
@ -12210,6 +12300,217 @@ def start_oauth_callback_servers():
|
|||
|
||||
print("✅ OAuth callback servers started")
|
||||
|
||||
# ===============================================
|
||||
# Artist Detail Spotify Integration Functions
|
||||
# ===============================================
|
||||
|
||||
def get_spotify_artist_discography(artist_name):
|
||||
"""Get complete artist discography from Spotify"""
|
||||
try:
|
||||
from core.spotify_client import SpotifyClient
|
||||
|
||||
print(f"🎵 Searching Spotify for artist: {artist_name}")
|
||||
|
||||
# Initialize Spotify client
|
||||
spotify_client = SpotifyClient()
|
||||
|
||||
# Search for the artist
|
||||
artists = spotify_client.search_artists(artist_name, limit=1)
|
||||
|
||||
if not artists:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Artist "{artist_name}" not found on Spotify'
|
||||
}
|
||||
|
||||
artist = artists[0]
|
||||
spotify_artist_id = artist.id
|
||||
|
||||
print(f"🎵 Found Spotify artist: {artist.name} (ID: {spotify_artist_id})")
|
||||
|
||||
# Get all albums (albums, singles, and compilations)
|
||||
all_albums = spotify_client.get_artist_albums(spotify_artist_id, album_type='album,single,compilation', limit=50)
|
||||
|
||||
if not all_albums:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'No albums found for artist "{artist_name}"'
|
||||
}
|
||||
|
||||
print(f"📀 Found {len(all_albums)} releases on Spotify")
|
||||
|
||||
# Categorize releases
|
||||
albums = []
|
||||
eps = []
|
||||
singles = []
|
||||
|
||||
for album in all_albums:
|
||||
# Use the Album object properties
|
||||
track_count = album.total_tracks
|
||||
|
||||
release_data = {
|
||||
'title': album.name,
|
||||
'year': album.release_date[:4] if album.release_date else None,
|
||||
'image_url': album.image_url,
|
||||
'spotify_id': album.id,
|
||||
'owned': False, # Will be updated when merging with owned data
|
||||
'track_count': track_count,
|
||||
'album_type': album.album_type.lower()
|
||||
}
|
||||
|
||||
# Categorize based on album type and track count
|
||||
album_type = album.album_type.lower()
|
||||
|
||||
if album_type == 'single' or track_count <= 3:
|
||||
singles.append(release_data)
|
||||
elif album_type == 'compilation' or (track_count <= 8 and track_count > 3):
|
||||
eps.append(release_data)
|
||||
else:
|
||||
albums.append(release_data)
|
||||
|
||||
print(f"📀 Categorized Spotify releases - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'albums': albums,
|
||||
'eps': eps,
|
||||
'singles': singles,
|
||||
'artist_image': artist.image_url if hasattr(artist, 'image_url') else None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error getting Spotify discography for {artist_name}: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def merge_discography_data(owned_releases, spotify_discography):
|
||||
"""Build discography using Spotify as source of truth, checking if we own each release"""
|
||||
try:
|
||||
print("🔄 Building discography using Spotify categorization...")
|
||||
|
||||
def normalize_title(title):
|
||||
"""Normalize title for comparison"""
|
||||
import re
|
||||
normalized = title.lower().strip()
|
||||
normalized = re.sub(r'[^\w\s]', '', normalized)
|
||||
normalized = re.sub(r'\s+', ' ', normalized)
|
||||
return normalized.strip()
|
||||
|
||||
def normalize_year(year):
|
||||
"""Normalize year to integer for comparison"""
|
||||
if year is None:
|
||||
return None
|
||||
try:
|
||||
return int(year)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
# Create a flat map of ALL owned releases (regardless of category)
|
||||
all_owned = []
|
||||
all_owned.extend(owned_releases['albums'])
|
||||
all_owned.extend(owned_releases['eps'])
|
||||
all_owned.extend(owned_releases['singles'])
|
||||
|
||||
owned_map = {}
|
||||
for owned in all_owned:
|
||||
key = (normalize_title(owned['title']), normalize_year(owned.get('year')))
|
||||
if key not in owned_map:
|
||||
owned_map[key] = []
|
||||
owned_map[key].append(owned)
|
||||
|
||||
print(f"📀 Created lookup map for {len(all_owned)} owned releases")
|
||||
|
||||
def build_category(spotify_category, category_name):
|
||||
"""Build cards for a category using Spotify as source of truth"""
|
||||
cards = []
|
||||
print(f"📀 Building {category_name} category with {len(spotify_category)} Spotify releases")
|
||||
|
||||
for spotify_release in spotify_category:
|
||||
spotify_key = (normalize_title(spotify_release['title']), normalize_year(spotify_release.get('year')))
|
||||
|
||||
# Debug logging for Bad Hair Day specifically
|
||||
if 'bad hair day' in spotify_release['title'].lower():
|
||||
print(f"🔍 DEBUG: Spotify 'Bad Hair Day' - Title: '{spotify_release['title']}', Year: {spotify_release.get('year')}")
|
||||
print(f"🔍 DEBUG: Normalized key: {spotify_key}")
|
||||
print(f"🔍 DEBUG: Available owned keys: {list(owned_map.keys())}")
|
||||
|
||||
# Check if we own this release (exact match first)
|
||||
owned_release = None
|
||||
matched_key = None
|
||||
|
||||
if spotify_key in owned_map and owned_map[spotify_key]:
|
||||
owned_release = owned_map[spotify_key].pop(0).copy()
|
||||
matched_key = spotify_key
|
||||
else:
|
||||
# Fallback: try matching by title only (ignore year)
|
||||
title_only = normalize_title(spotify_release['title'])
|
||||
for key, releases in owned_map.items():
|
||||
if key[0] == title_only and releases: # key[0] is the normalized title
|
||||
owned_release = releases.pop(0).copy()
|
||||
matched_key = key
|
||||
print(f"🔄 Year mismatch fallback: '{spotify_release['title']}' matched by title only")
|
||||
break
|
||||
|
||||
if owned_release:
|
||||
# We own it - use owned data with Spotify enhancements
|
||||
|
||||
# Add Spotify metadata
|
||||
owned_release['spotify_id'] = spotify_release['spotify_id']
|
||||
owned_release['owned'] = True
|
||||
|
||||
# Image priority: owned first, then Spotify fallback
|
||||
if not owned_release.get('image_url') and spotify_release.get('image_url'):
|
||||
owned_release['image_url'] = spotify_release['image_url']
|
||||
|
||||
cards.append(owned_release)
|
||||
print(f"✅ {category_name}: '{spotify_release['title']}' - OWNED")
|
||||
|
||||
# Remove empty lists from map (use the key that actually matched)
|
||||
if matched_key and not owned_map[matched_key]:
|
||||
del owned_map[matched_key]
|
||||
else:
|
||||
# We don't own it - create missing card
|
||||
missing_release = spotify_release.copy()
|
||||
missing_release['owned'] = False
|
||||
missing_release['track_completion'] = 0
|
||||
cards.append(missing_release)
|
||||
print(f"❌ {category_name}: '{spotify_release['title']}' - MISSING")
|
||||
|
||||
return cards
|
||||
|
||||
# Build each category using Spotify as the source of truth
|
||||
albums = build_category(spotify_discography['albums'], 'Albums')
|
||||
eps = build_category(spotify_discography['eps'], 'EPs')
|
||||
singles = build_category(spotify_discography['singles'], 'Singles')
|
||||
|
||||
# Report any owned releases that didn't match Spotify (rare)
|
||||
remaining_owned = sum(len(owned_list) for owned_list in owned_map.values())
|
||||
if remaining_owned > 0:
|
||||
print(f"📀 Note: {remaining_owned} owned releases not found on Spotify (compilations, bootlegs, etc.)")
|
||||
|
||||
print(f"✅ Built discography - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'albums': albums,
|
||||
'eps': eps,
|
||||
'singles': singles
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error building discography: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'albums': [],
|
||||
'eps': [],
|
||||
'singles': []
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Initialize logging for web server
|
||||
from utils.logging_config import setup_logging
|
||||
|
|
|
|||
135
webui/index.html
135
webui/index.html
|
|
@ -588,10 +588,10 @@
|
|||
</button>
|
||||
|
||||
<div class="artist-detail-info">
|
||||
<div class="artist-detail-image" id="artist-detail-image"></div>
|
||||
<div class="artist-detail-image" id="search-artist-detail-image"></div>
|
||||
<div class="artist-detail-text">
|
||||
<h2 class="artist-detail-name" id="artist-detail-name">Artist Name</h2>
|
||||
<p class="artist-detail-genres" id="artist-detail-genres">Genres</p>
|
||||
<h2 class="artist-detail-name" id="search-artist-detail-name">Artist Name</h2>
|
||||
<p class="artist-detail-genres" id="search-artist-detail-genres">Genres</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -729,6 +729,135 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Artist Detail Page -->
|
||||
<div class="page" id="artist-detail-page">
|
||||
<div class="page-header">
|
||||
<button class="back-btn" id="artist-detail-back-btn">
|
||||
<span>← Back to Library</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Artist Hero Section -->
|
||||
<div class="artist-hero-section">
|
||||
<div class="artist-hero-content">
|
||||
<div class="artist-image-container">
|
||||
<img class="artist-image" id="artist-detail-image" src="" alt="Artist Image" />
|
||||
<div class="artist-image-fallback" id="artist-detail-image-fallback">🎵</div>
|
||||
</div>
|
||||
|
||||
<div class="artist-info">
|
||||
<h1 class="artist-name" id="artist-detail-name">Artist Name</h1>
|
||||
|
||||
<div class="collection-overview">
|
||||
<div class="collection-category">
|
||||
<div class="category-header">
|
||||
<span class="category-label">Albums</span>
|
||||
<span class="category-stats" id="albums-stats">0 owned, 0 missing</span>
|
||||
</div>
|
||||
<div class="completion-section">
|
||||
<div class="completion-bar">
|
||||
<div class="completion-fill" id="albums-completion-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="completion-text" id="albums-completion-text">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collection-category">
|
||||
<div class="category-header">
|
||||
<span class="category-label">EPs</span>
|
||||
<span class="category-stats" id="eps-stats">0 owned, 0 missing</span>
|
||||
</div>
|
||||
<div class="completion-section">
|
||||
<div class="completion-bar">
|
||||
<div class="completion-fill" id="eps-completion-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="completion-text" id="eps-completion-text">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collection-category">
|
||||
<div class="category-header">
|
||||
<span class="category-label">Singles</span>
|
||||
<span class="category-stats" id="singles-stats">0 owned, 0 missing</span>
|
||||
</div>
|
||||
<div class="completion-section">
|
||||
<div class="completion-bar">
|
||||
<div class="completion-fill" id="singles-completion-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="completion-text" id="singles-completion-text">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="artist-detail-content">
|
||||
<!-- Loading State -->
|
||||
<div class="artist-detail-loading hidden" id="artist-detail-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading artist discography...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div class="artist-detail-error hidden" id="artist-detail-error">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<h3>Failed to load artist details</h3>
|
||||
<p id="artist-detail-error-message">An error occurred while loading the artist's discography.</p>
|
||||
<button class="retry-btn" id="artist-detail-retry-btn">Retry</button>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="artist-detail-main" id="artist-detail-main">
|
||||
|
||||
<!-- Discography Sections -->
|
||||
<div class="discography-sections">
|
||||
<!-- Albums Section -->
|
||||
<div class="discography-section" id="albums-section">
|
||||
<div class="section-header">
|
||||
<h3>Albums</h3>
|
||||
<div class="section-stats">
|
||||
<span id="albums-owned-count">0 owned</span>
|
||||
<span id="albums-missing-count">0 missing</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="releases-grid" id="albums-grid">
|
||||
<!-- Album cards will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EPs Section -->
|
||||
<div class="discography-section" id="eps-section">
|
||||
<div class="section-header">
|
||||
<h3>EPs</h3>
|
||||
<div class="section-stats">
|
||||
<span id="eps-owned-count">0 owned</span>
|
||||
<span id="eps-missing-count">0 missing</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="releases-grid" id="eps-grid">
|
||||
<!-- EP cards will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Singles Section -->
|
||||
<div class="discography-section" id="singles-section">
|
||||
<div class="section-header">
|
||||
<h3>Singles</h3>
|
||||
<div class="section-stats">
|
||||
<span id="singles-owned-count">0 owned</span>
|
||||
<span id="singles-missing-count">0 missing</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="releases-grid" id="singles-grid">
|
||||
<!-- Single cards will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Page -->
|
||||
<div class="page" id="settings-page">
|
||||
<div class="page-header">
|
||||
|
|
|
|||
|
|
@ -346,11 +346,14 @@ function initializeWatchlist() {
|
|||
function navigateToPage(pageId) {
|
||||
if (pageId === currentPage) return;
|
||||
|
||||
// Update navigation buttons
|
||||
// Update navigation buttons (only if there's a nav button for this page)
|
||||
document.querySelectorAll('.nav-button').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
document.querySelector(`[data-page="${pageId}"]`).classList.add('active');
|
||||
const navButton = document.querySelector(`[data-page="${pageId}"]`);
|
||||
if (navButton) {
|
||||
navButton.classList.add('active');
|
||||
}
|
||||
|
||||
// Update pages
|
||||
document.querySelectorAll('.page').forEach(page => {
|
||||
|
|
@ -405,6 +408,9 @@ async function loadPageData(pageId) {
|
|||
await loadLibraryArtists();
|
||||
}
|
||||
break;
|
||||
case 'artist-detail':
|
||||
// Artist detail page is handled separately by navigateToArtistDetail()
|
||||
break;
|
||||
case 'settings':
|
||||
initializeSettings();
|
||||
await loadSettingsData();
|
||||
|
|
@ -13824,11 +13830,10 @@ function createLibraryArtistCard(artist) {
|
|||
card.appendChild(imageContainer);
|
||||
card.appendChild(info);
|
||||
|
||||
// Add click handler (for future functionality)
|
||||
// Add click handler to navigate to artist detail page
|
||||
card.addEventListener("click", () => {
|
||||
// TODO: Implement artist detail view
|
||||
console.log(`Clicked on artist: ${artist.name}`);
|
||||
showToast(`Artist details coming soon!`, "info");
|
||||
console.log(`🎵 Opening artist detail for: ${artist.name} (ID: ${artist.id})`);
|
||||
navigateToArtistDetail(artist.id, artist.name);
|
||||
});
|
||||
|
||||
return card;
|
||||
|
|
@ -13892,3 +13897,475 @@ function showLibraryEmpty(show) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// Artist Detail Page Functions
|
||||
// ===============================================
|
||||
|
||||
// Artist detail page state
|
||||
let artistDetailPageState = {
|
||||
isInitialized: false,
|
||||
currentArtistId: null,
|
||||
currentArtistName: null
|
||||
};
|
||||
|
||||
function navigateToArtistDetail(artistId, artistName) {
|
||||
console.log(`🎵 Navigating to artist detail: ${artistName} (ID: ${artistId})`);
|
||||
|
||||
// Store current artist info
|
||||
artistDetailPageState.currentArtistId = artistId;
|
||||
artistDetailPageState.currentArtistName = artistName;
|
||||
|
||||
// Navigate to artist detail page
|
||||
navigateToPage('artist-detail');
|
||||
|
||||
// Initialize if needed and load data
|
||||
if (!artistDetailPageState.isInitialized) {
|
||||
initializeArtistDetailPage();
|
||||
}
|
||||
|
||||
// Load artist data
|
||||
loadArtistDetailData(artistId, artistName);
|
||||
}
|
||||
|
||||
function initializeArtistDetailPage() {
|
||||
console.log("🔧 Initializing Artist Detail page...");
|
||||
|
||||
// Initialize back button
|
||||
const backBtn = document.getElementById("artist-detail-back-btn");
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener("click", () => {
|
||||
console.log("🔙 Returning to Library page");
|
||||
navigateToPage('library');
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize retry button
|
||||
const retryBtn = document.getElementById("artist-detail-retry-btn");
|
||||
if (retryBtn) {
|
||||
retryBtn.addEventListener("click", () => {
|
||||
if (artistDetailPageState.currentArtistId && artistDetailPageState.currentArtistName) {
|
||||
loadArtistDetailData(artistDetailPageState.currentArtistId, artistDetailPageState.currentArtistName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
artistDetailPageState.isInitialized = true;
|
||||
console.log("✅ Artist Detail page initialized successfully");
|
||||
}
|
||||
|
||||
async function loadArtistDetailData(artistId, artistName) {
|
||||
console.log(`🔄 Loading artist detail data for: ${artistName} (ID: ${artistId})`);
|
||||
|
||||
// Show loading state
|
||||
showArtistDetailLoading(true);
|
||||
showArtistDetailError(false);
|
||||
showArtistDetailMain(false);
|
||||
|
||||
// Update header with artist name
|
||||
updateArtistDetailHeader(artistName);
|
||||
|
||||
try {
|
||||
// Call API to get artist discography data
|
||||
const response = await fetch(`/api/artist-detail/${artistId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load artist data: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to load artist data');
|
||||
}
|
||||
|
||||
console.log(`✅ Loaded artist detail data:`, data);
|
||||
|
||||
// Hide loading and show main content
|
||||
showArtistDetailLoading(false);
|
||||
showArtistDetailMain(true);
|
||||
|
||||
console.log(`🎨 Main content visibility:`, document.getElementById('artist-detail-main'));
|
||||
console.log(`🎨 Albums section:`, document.getElementById('albums-section'));
|
||||
|
||||
// Populate the page with data
|
||||
populateArtistDetailPage(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error loading artist detail data:`, error);
|
||||
|
||||
// Show error state
|
||||
showArtistDetailLoading(false);
|
||||
showArtistDetailError(true, error.message);
|
||||
|
||||
showToast(`Failed to load artist details: ${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function updateArtistDetailHeader(artistName) {
|
||||
// Update header title
|
||||
const headerTitle = document.getElementById("artist-detail-name");
|
||||
if (headerTitle) {
|
||||
headerTitle.textContent = artistName;
|
||||
}
|
||||
|
||||
// Update main artist name
|
||||
const mainTitle = document.getElementById("artist-info-name");
|
||||
if (mainTitle) {
|
||||
mainTitle.textContent = artistName;
|
||||
}
|
||||
}
|
||||
|
||||
function populateArtistDetailPage(data) {
|
||||
const artist = data.artist;
|
||||
const discography = data.discography;
|
||||
|
||||
console.log(`🎨 Populating artist detail page for: ${artist.name}`);
|
||||
console.log(`📀 Discography data:`, discography);
|
||||
console.log(`📀 Albums:`, discography.albums);
|
||||
console.log(`📀 EPs:`, discography.eps);
|
||||
console.log(`📀 Singles:`, discography.singles);
|
||||
|
||||
// Update hero section with image, name, and stats
|
||||
updateArtistHeroSection(artist, discography);
|
||||
|
||||
// Update genres (if element exists)
|
||||
updateArtistGenres(artist.genres);
|
||||
|
||||
// Update summary stats (if element exists)
|
||||
updateArtistSummaryStats(discography);
|
||||
|
||||
// Populate discography sections
|
||||
populateDiscographySections(discography);
|
||||
}
|
||||
|
||||
function updateArtistDetailImage(imageUrl, artistName) {
|
||||
const imageElement = document.getElementById("artist-detail-image");
|
||||
const fallbackElement = document.getElementById("artist-image-fallback");
|
||||
|
||||
if (imageUrl && imageUrl.trim() !== "") {
|
||||
imageElement.src = imageUrl;
|
||||
imageElement.alt = artistName;
|
||||
imageElement.classList.remove("hidden");
|
||||
fallbackElement.classList.add("hidden");
|
||||
|
||||
imageElement.onerror = () => {
|
||||
console.log(`Failed to load artist image for ${artistName}: ${imageUrl}`);
|
||||
// Replace with fallback on error
|
||||
imageElement.classList.add("hidden");
|
||||
fallbackElement.classList.remove("hidden");
|
||||
};
|
||||
|
||||
imageElement.onload = () => {
|
||||
console.log(`Successfully loaded artist image for ${artistName}: ${imageUrl}`);
|
||||
};
|
||||
} else {
|
||||
console.log(`No image URL for ${artistName}: '${imageUrl}'`);
|
||||
imageElement.classList.add("hidden");
|
||||
fallbackElement.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function updateArtistGenres(genres) {
|
||||
const genresContainer = document.getElementById("artist-genres");
|
||||
if (!genresContainer) return;
|
||||
|
||||
genresContainer.innerHTML = "";
|
||||
|
||||
if (genres && genres.length > 0) {
|
||||
genres.forEach(genre => {
|
||||
const genreTag = document.createElement("span");
|
||||
genreTag.className = "genre-tag";
|
||||
genreTag.textContent = genre;
|
||||
genresContainer.appendChild(genreTag);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateArtistSummaryStats(discography) {
|
||||
// Calculate stats
|
||||
const ownedAlbums = discography.albums.filter(album => album.owned).length;
|
||||
const missingAlbums = discography.albums.filter(album => !album.owned).length;
|
||||
const totalAlbums = discography.albums.length;
|
||||
const completionPercentage = totalAlbums > 0 ? Math.round((ownedAlbums / totalAlbums) * 100) : 0;
|
||||
|
||||
// Update owned albums count
|
||||
const ownedElement = document.getElementById("owned-albums-count");
|
||||
if (ownedElement) {
|
||||
ownedElement.textContent = ownedAlbums;
|
||||
}
|
||||
|
||||
// Update missing albums count
|
||||
const missingElement = document.getElementById("missing-albums-count");
|
||||
if (missingElement) {
|
||||
missingElement.textContent = missingAlbums;
|
||||
}
|
||||
|
||||
// Update completion percentage
|
||||
const completionElement = document.getElementById("completion-percentage");
|
||||
if (completionElement) {
|
||||
completionElement.textContent = `${completionPercentage}%`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateArtistHeaderStats(albumCount, trackCount) {
|
||||
// This function is deprecated - now using updateArtistHeroSection
|
||||
console.log("📊 Using new hero section instead of old header stats");
|
||||
}
|
||||
|
||||
function updateArtistHeroSection(artist, discography) {
|
||||
console.log("🖼️ Updating artist hero section");
|
||||
|
||||
// Update artist image with detailed debugging
|
||||
const imageElement = document.getElementById("artist-detail-image");
|
||||
const fallbackElement = document.getElementById("artist-detail-image-fallback");
|
||||
|
||||
console.log(`🖼️ Debug Artist image info:`);
|
||||
console.log(` - URL: '${artist.image_url}'`);
|
||||
console.log(` - Type: ${typeof artist.image_url}`);
|
||||
console.log(` - Full artist object:`, artist);
|
||||
console.log(` - Image element:`, imageElement);
|
||||
console.log(` - Fallback element:`, fallbackElement);
|
||||
|
||||
if (artist.image_url && artist.image_url.trim() !== "" && artist.image_url !== "null") {
|
||||
console.log(`✅ Setting image src to: ${artist.image_url}`);
|
||||
imageElement.src = artist.image_url;
|
||||
imageElement.alt = artist.name;
|
||||
imageElement.style.display = "block";
|
||||
if (fallbackElement) {
|
||||
fallbackElement.style.display = "none";
|
||||
}
|
||||
|
||||
imageElement.onload = () => {
|
||||
console.log(`✅ Successfully loaded artist image: ${artist.image_url}`);
|
||||
};
|
||||
|
||||
imageElement.onerror = () => {
|
||||
console.error(`❌ Failed to load artist image: ${artist.image_url}`);
|
||||
imageElement.style.display = "none";
|
||||
if (fallbackElement) {
|
||||
fallbackElement.style.display = "flex";
|
||||
}
|
||||
};
|
||||
} else {
|
||||
console.log(`🖼️ No valid image URL - showing fallback for ${artist.name}`);
|
||||
imageElement.style.display = "none";
|
||||
if (fallbackElement) {
|
||||
fallbackElement.style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
// Update artist name
|
||||
const nameElement = document.getElementById("artist-detail-name");
|
||||
if (nameElement) {
|
||||
nameElement.textContent = artist.name;
|
||||
}
|
||||
|
||||
// Calculate and update stats for each category
|
||||
updateCategoryStats('albums', discography.albums);
|
||||
updateCategoryStats('eps', discography.eps);
|
||||
updateCategoryStats('singles', discography.singles);
|
||||
}
|
||||
|
||||
function updateCategoryStats(category, releases) {
|
||||
const owned = releases.filter(r => r.owned !== false).length;
|
||||
const missing = releases.filter(r => r.owned === false).length;
|
||||
const total = releases.length;
|
||||
const completion = total > 0 ? Math.round((owned / total) * 100) : 0;
|
||||
|
||||
console.log(`📊 ${category}: ${owned} owned, ${missing} missing, ${completion}% complete`);
|
||||
|
||||
// Update stats text
|
||||
const statsElement = document.getElementById(`${category}-stats`);
|
||||
if (statsElement) {
|
||||
statsElement.textContent = `${owned} owned, ${missing} missing`;
|
||||
}
|
||||
|
||||
// Update completion bar
|
||||
const fillElement = document.getElementById(`${category}-completion-fill`);
|
||||
if (fillElement) {
|
||||
fillElement.style.width = `${completion}%`;
|
||||
}
|
||||
|
||||
// Update completion text
|
||||
const textElement = document.getElementById(`${category}-completion-text`);
|
||||
if (textElement) {
|
||||
textElement.textContent = `${completion}%`;
|
||||
}
|
||||
}
|
||||
|
||||
function populateDiscographySections(discography) {
|
||||
// Populate albums
|
||||
populateReleaseSection('albums', discography.albums);
|
||||
|
||||
// Populate EPs
|
||||
populateReleaseSection('eps', discography.eps);
|
||||
|
||||
// Populate singles
|
||||
populateReleaseSection('singles', discography.singles);
|
||||
}
|
||||
|
||||
function populateReleaseSection(sectionType, releases) {
|
||||
const gridId = `${sectionType}-grid`;
|
||||
const ownedCountId = `${sectionType}-owned-count`;
|
||||
const missingCountId = `${sectionType}-missing-count`;
|
||||
|
||||
const grid = document.getElementById(gridId);
|
||||
if (!grid) return;
|
||||
|
||||
// Clear existing content
|
||||
grid.innerHTML = "";
|
||||
|
||||
// Calculate stats
|
||||
const ownedCount = releases.filter(release => release.owned).length;
|
||||
const missingCount = releases.filter(release => !release.owned).length;
|
||||
|
||||
// Update section stats
|
||||
const ownedElement = document.getElementById(ownedCountId);
|
||||
const missingElement = document.getElementById(missingCountId);
|
||||
|
||||
if (ownedElement) {
|
||||
ownedElement.textContent = `${ownedCount} owned`;
|
||||
}
|
||||
|
||||
if (missingElement) {
|
||||
missingElement.textContent = `${missingCount} missing`;
|
||||
}
|
||||
|
||||
// Create release cards
|
||||
releases.forEach((release, index) => {
|
||||
console.log(`📀 Creating card ${index + 1} for: ${release.title}`);
|
||||
const card = createReleaseCard(release);
|
||||
grid.appendChild(card);
|
||||
console.log(`📀 Added card to grid:`, card);
|
||||
});
|
||||
|
||||
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");
|
||||
card.className = `release-card${release.owned ? "" : " missing"}`;
|
||||
card.setAttribute("data-release-id", release.id || "");
|
||||
card.setAttribute("data-spotify-id", release.spotify_id || "");
|
||||
|
||||
// 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.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
|
||||
const year = document.createElement("div");
|
||||
year.className = "release-year";
|
||||
year.textContent = release.year || "Unknown Year";
|
||||
|
||||
// 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) {
|
||||
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 {
|
||||
completionText.textContent = "Missing";
|
||||
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 click handler for future functionality
|
||||
card.addEventListener("click", () => {
|
||||
console.log(`Clicked on release: ${release.title} (Owned: ${release.owned})`);
|
||||
if (release.owned) {
|
||||
showToast(`Album details coming soon!`, "info");
|
||||
} else {
|
||||
showToast(`Add to download queue coming soon!`, "info");
|
||||
}
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// UI state management functions
|
||||
function showArtistDetailLoading(show) {
|
||||
const loadingElement = document.getElementById("artist-detail-loading");
|
||||
if (loadingElement) {
|
||||
if (show) {
|
||||
loadingElement.classList.remove("hidden");
|
||||
} else {
|
||||
loadingElement.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showArtistDetailError(show, message = "") {
|
||||
const errorElement = document.getElementById("artist-detail-error");
|
||||
const errorMessageElement = document.getElementById("artist-detail-error-message");
|
||||
|
||||
if (errorElement) {
|
||||
if (show) {
|
||||
errorElement.classList.remove("hidden");
|
||||
if (errorMessageElement && message) {
|
||||
errorMessageElement.textContent = message;
|
||||
}
|
||||
} else {
|
||||
errorElement.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showArtistDetailMain(show) {
|
||||
const mainElement = document.getElementById("artist-detail-main");
|
||||
if (mainElement) {
|
||||
if (show) {
|
||||
mainElement.classList.remove("hidden");
|
||||
} else {
|
||||
mainElement.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8870,3 +8870,670 @@ body {
|
|||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Artist Detail Page Styles
|
||||
======================================== */
|
||||
|
||||
.artist-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%);
|
||||
backdrop-filter: blur(12px) saturate(1.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 12px 20px;
|
||||
color: #ffffff;
|
||||
font-family: 'SF Pro Text', -apple-system, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: linear-gradient(135deg, rgba(40, 40, 40, 0.95) 0%, rgba(30, 30, 30, 0.98) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.artist-detail-title {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.artist-detail-title h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.artist-detail-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.artist-stat {
|
||||
color: #b3b3b3;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Artist Detail Content */
|
||||
.artist-detail-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.artist-detail-loading,
|
||||
.artist-detail-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.artist-detail-loading .loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||
border-top: 3px solid #1db954;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.artist-detail-error .error-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.artist-detail-error h3 {
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.artist-detail-error p {
|
||||
color: #b3b3b3;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: linear-gradient(135deg, #1db954 0%, #1ed760 100%);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 12px 24px;
|
||||
color: #ffffff;
|
||||
font-family: 'SF Pro Text', -apple-system, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: linear-gradient(135deg, #1ed760 0%, #1db954 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 25px rgba(29, 185, 84, 0.3);
|
||||
}
|
||||
|
||||
/* Artist Info Section */
|
||||
.artist-info-section {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
padding: 30px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 30px;
|
||||
background: linear-gradient(135deg, rgba(26, 26, 26, 0.3) 0%, rgba(18, 18, 18, 0.5) 100%);
|
||||
border-radius: 20px;
|
||||
padding: 32px;
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.artist-image-container {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.artist-detail-image {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 16px;
|
||||
object-fit: cover;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.artist-image-fallback {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.artist-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.artist-info h1 {
|
||||
font-size: 48px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.artist-genres {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.genre-tag {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 20px;
|
||||
padding: 6px 14px;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.artist-summary-stats {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.summary-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: #1db954;
|
||||
line-height: 1;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #b3b3b3;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Discography Sections */
|
||||
.discography-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.discography-section {
|
||||
opacity: 1; /* Show immediately instead of animation for now */
|
||||
}
|
||||
|
||||
/* Animation keyframes for future use */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: #b3b3b3;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.releases-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Release Cards */
|
||||
.release-card {
|
||||
background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%);
|
||||
backdrop-filter: blur(12px) saturate(1.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 280px; /* Fixed height for consistent layout */
|
||||
}
|
||||
|
||||
.release-card:hover {
|
||||
background: linear-gradient(135deg, rgba(40, 40, 40, 0.95) 0%, rgba(30, 30, 30, 0.98) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.release-card.missing {
|
||||
opacity: 0.6;
|
||||
background: linear-gradient(135deg, rgba(60, 60, 60, 0.3) 0%, rgba(40, 40, 40, 0.5) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.release-card.missing:hover {
|
||||
opacity: 0.8;
|
||||
background: linear-gradient(135deg, rgba(70, 70, 70, 0.4) 0%, rgba(50, 50, 50, 0.6) 100%);
|
||||
}
|
||||
|
||||
.release-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.release-image-fallback {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(60, 60, 60, 0.6) 0%, rgba(40, 40, 40, 0.8) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.release-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 6px 0;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.release-year {
|
||||
font-size: 14px;
|
||||
color: #b3b3b3;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.release-completion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
margin-top: auto;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.completion-text {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.completion-text.complete {
|
||||
color: #1db954;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.completion-text.partial {
|
||||
color: #ffa500;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.completion-text.missing {
|
||||
color: #ff6b6b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.completion-bar {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.completion-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
}
|
||||
|
||||
.completion-fill.complete {
|
||||
background: linear-gradient(90deg, #1db954, #1ed760);
|
||||
}
|
||||
|
||||
.completion-fill.partial {
|
||||
background: linear-gradient(90deg, #ffa500, #ffb347);
|
||||
}
|
||||
|
||||
.completion-fill.missing {
|
||||
background: linear-gradient(90deg, #ff6b6b, #ff8e8e);
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.artist-detail-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.artist-detail-title h2 {
|
||||
font-size: 24px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.artist-info-section {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.artist-detail-image,
|
||||
.artist-image-fallback {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.artist-info h1 {
|
||||
font-size: 36px;
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.artist-summary-stats {
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.releases-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.release-card {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
ARTIST HERO SECTION (Modal-inspired design)
|
||||
=============================== */
|
||||
|
||||
.artist-hero-section {
|
||||
background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%);
|
||||
backdrop-filter: blur(12px) saturate(1.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
margin: 20px 20px 20px 20px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.artist-hero-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.artist-image-container {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.artist-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
border: 2px solid rgba(29, 185, 84, 0.3);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.artist-image-fallback {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(29, 185, 84, 0.2) 0%, rgba(30, 215, 96, 0.1) 100%);
|
||||
border: 2px solid rgba(29, 185, 84, 0.3);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.artist-image:not([src=""]):not([src]) + .artist-image-fallback,
|
||||
.artist-image[src]:not([src=""]) + .artist-image-fallback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.artist-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.artist-name {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.1;
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.collection-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.collection-category {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.collection-category:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(29, 185, 84, 0.2);
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.category-label {
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.category-stats {
|
||||
color: #b3b3b3;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.completion-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.completion-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.completion-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #1db954 0%, #1ed760 100%);
|
||||
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.completion-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.2) 50%, transparent 100%);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.completion-text {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
min-width: 45px;
|
||||
text-align: right;
|
||||
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.artist-hero-section {
|
||||
margin: 15px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.artist-hero-content {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.artist-image,
|
||||
.artist-image-fallback {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.artist-name {
|
||||
font-size: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.completion-section {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue