artists page

This commit is contained in:
Broque Thomas 2025-09-03 18:02:44 -07:00
parent 57b0833fe2
commit c29c5d6495
4 changed files with 1613 additions and 6 deletions

View file

@ -1587,6 +1587,91 @@ def get_artists():
]
return jsonify({"artists": mock_artists})
@app.route('/api/artist/<artist_id>/discography', methods=['GET'])
def get_artist_discography(artist_id):
"""Get an artist's complete discography (albums and singles)"""
try:
if not spotify_client or not spotify_client.is_authenticated():
return jsonify({"error": "Spotify not authenticated"}), 401
print(f"🎤 Fetching discography for artist: {artist_id}")
# Get all albums and singles for the artist
albums = spotify_client.get_artist_albums(artist_id, album_type='album,single,appears_on', limit=50)
if not albums:
return jsonify({
"albums": [],
"singles": []
})
# Separate albums from singles/EPs
album_list = []
singles_list = []
# Track seen albums to avoid duplicates (especially for "appears_on")
seen_albums = set()
for album in albums:
# Skip duplicates
if album.id in seen_albums:
continue
seen_albums.add(album.id)
# Skip compilations and appears_on that aren't the main artist's work
if hasattr(album, 'album_type') and album.album_type == 'appears_on':
# Only include if the artist is the main artist
if hasattr(album, 'artists') and album.artists:
main_artist_id = album.artists[0].id if hasattr(album.artists[0], 'id') else None
if main_artist_id != artist_id:
continue
album_data = {
"id": album.id,
"name": album.name,
"release_date": album.release_date if hasattr(album, 'release_date') else None,
"album_type": album.album_type if hasattr(album, 'album_type') else 'album',
"image_url": album.image_url if hasattr(album, 'image_url') else None,
"total_tracks": album.total_tracks if hasattr(album, 'total_tracks') else 0,
"external_urls": album.external_urls if hasattr(album, 'external_urls') else {}
}
# Categorize by album type
if hasattr(album, 'album_type'):
if album.album_type in ['single', 'ep']:
singles_list.append(album_data)
else: # 'album' or 'compilation'
album_list.append(album_data)
else:
# Default to album if no type specified
album_list.append(album_data)
# Sort by release date (newest first)
def get_release_year(item):
if item['release_date']:
try:
# Handle different date formats (YYYY, YYYY-MM, YYYY-MM-DD)
return int(item['release_date'][:4])
except (ValueError, IndexError):
return 0
return 0
album_list.sort(key=get_release_year, reverse=True)
singles_list.sort(key=get_release_year, reverse=True)
print(f"✅ Found {len(album_list)} albums and {len(singles_list)} singles for artist")
return jsonify({
"albums": album_list,
"singles": singles_list
})
except Exception as e:
print(f"❌ Error fetching artist discography: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@app.route('/api/stream/start', methods=['POST'])
def stream_start():
"""Start streaming a track in the background"""

View file

@ -528,12 +528,94 @@
<!-- Artists Page -->
<div class="page" id="artists-page">
<div class="page-header">
<h2>Artists</h2>
<!-- Initial Search State -->
<div class="artists-search-state" id="artists-search-state">
<div class="artists-search-container">
<div class="artists-welcome-section">
<h2 class="artists-welcome-title">🎵 Discover Artists</h2>
<p class="artists-welcome-subtitle">Search for your favorite artists and explore their complete discography</p>
</div>
<div class="artists-search-input-container">
<input type="text"
id="artists-search-input"
class="artists-search-input"
placeholder="Search for an artist...">
<div class="artists-search-icon">🔍</div>
</div>
<div class="artists-search-status" id="artists-search-status">
Start typing to search for artists
</div>
</div>
</div>
<div class="artists-content">
<div class="artists-grid" id="artists-grid">
<!-- Artists will be populated here -->
<!-- Search Results State -->
<div class="artists-results-state hidden" id="artists-results-state">
<div class="artists-results-header">
<button class="artists-back-button" id="artists-back-button">
<span class="back-icon"></span>
<span>Back to Search</span>
</button>
<div class="artists-search-header">
<input type="text"
id="artists-header-search-input"
class="artists-header-search-input"
placeholder="Search for an artist...">
</div>
</div>
<div class="artists-results-content">
<div class="artists-results-title">Search Results</div>
<div class="artists-cards-container" id="artists-cards-container">
<!-- Artist cards will be dynamically populated here -->
</div>
</div>
</div>
<!-- Artist Detail State -->
<div class="artist-detail-state hidden" id="artist-detail-state">
<div class="artist-detail-header">
<button class="artist-detail-back-button" id="artist-detail-back-button">
<span class="back-icon"></span>
<span>Back to Results</span>
</button>
<div class="artist-detail-info">
<div class="artist-detail-image" id="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>
</div>
</div>
</div>
<div class="artist-detail-content">
<div class="artist-detail-tabs">
<button class="artist-tab active" data-tab="albums" id="albums-tab">
<span class="tab-icon">💿</span>
<span>Albums</span>
</button>
<button class="artist-tab" data-tab="singles" id="singles-tab">
<span class="tab-icon">🎵</span>
<span>Singles & EPs</span>
</button>
</div>
<div class="artist-detail-discography">
<div class="tab-content active" id="albums-content">
<div class="album-cards-container" id="album-cards-container">
<!-- Album cards will be populated here -->
</div>
</div>
<div class="tab-content" id="singles-content">
<div class="singles-cards-container" id="singles-cards-container">
<!-- Singles cards will be populated here -->
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -47,6 +47,24 @@ let tidalPlaylists = [];
let tidalPlaylistStates = {}; // Key: playlist_id, Value: playlist state with phases
let tidalPlaylistsLoaded = false;
// --- Artists Page State Management ---
let artistsPageState = {
currentView: 'search', // 'search', 'results', 'detail'
searchQuery: '',
searchResults: [],
selectedArtist: null,
artistDiscography: {
albums: [],
singles: []
},
cache: {
searches: {}, // Cache search results by query
discography: {} // Cache discography by artist ID
}
};
let artistsSearchTimeout = null;
let artistsSearchController = null;
// --- Wishlist Modal Persistence State Management ---
const WishlistModalState = {
// Track if wishlist modal was visible before page refresh
@ -343,7 +361,7 @@ async function loadPageData(pageId) {
break;
case 'artists':
stopDownloadPolling();
await loadArtistsData();
initializeArtistsPage();
break;
case 'settings':
initializeSettings();
@ -5183,6 +5201,7 @@ window.selectAlbum = selectAlbum;
window.navigateToPage = navigateToPage;
window.openKofi = openKofi;
window.copyAddress = copyAddress;
window.retryLastSearch = retryLastSearch;
window.showVersionInfo = showVersionInfo;
window.closeVersionModal = closeVersionModal;
window.testConnection = testConnection;
@ -9040,6 +9059,673 @@ async function resetYouTubePlaylist(urlHash) {
}
}
// ============================================================================
// ARTISTS PAGE FUNCTIONALITY - ELEGANT SEARCH & DISCOVERY
// ============================================================================
/**
* Initialize the artists page when navigated to
*/
function initializeArtistsPage() {
console.log('🎵 Initializing Artists Page');
// Get DOM elements
const searchInput = document.getElementById('artists-search-input');
const headerSearchInput = document.getElementById('artists-header-search-input');
const searchStatus = document.getElementById('artists-search-status');
const backButton = document.getElementById('artists-back-button');
const detailBackButton = document.getElementById('artist-detail-back-button');
// Set up event listeners
if (searchInput) {
searchInput.addEventListener('input', handleArtistsSearchInput);
searchInput.addEventListener('keypress', handleArtistsSearchKeypress);
}
if (headerSearchInput) {
headerSearchInput.addEventListener('input', handleArtistsHeaderSearchInput);
headerSearchInput.addEventListener('keypress', handleArtistsSearchKeypress);
}
if (backButton) {
backButton.addEventListener('click', () => showArtistsSearchState());
}
if (detailBackButton) {
detailBackButton.addEventListener('click', () => showArtistsResultsState());
}
// Initialize tabs
initializeArtistTabs();
// Reset to search state
showArtistsSearchState();
console.log('✅ Artists Page initialized successfully');
}
/**
* Handle search input with debouncing
*/
function handleArtistsSearchInput(event) {
const query = event.target.value.trim();
updateArtistsSearchStatus('searching');
// Clear existing timeout
if (artistsSearchTimeout) {
clearTimeout(artistsSearchTimeout);
}
// Cancel any active search
if (artistsSearchController) {
artistsSearchController.abort();
}
if (query === '') {
updateArtistsSearchStatus('default');
return;
}
// Set up new debounced search
artistsSearchTimeout = setTimeout(() => {
performArtistsSearch(query);
}, 1000); // 1 second debounce
}
/**
* Handle header search input (already in results state)
*/
function handleArtistsHeaderSearchInput(event) {
const query = event.target.value.trim();
// Update main search input to match
const mainInput = document.getElementById('artists-search-input');
if (mainInput) {
mainInput.value = query;
}
// Trigger search with same debouncing logic
handleArtistsSearchInput(event);
}
/**
* Handle Enter key press in search inputs
*/
function handleArtistsSearchKeypress(event) {
if (event.key === 'Enter') {
event.preventDefault();
const query = event.target.value.trim();
if (query && query !== artistsPageState.searchQuery) {
// Clear timeout and search immediately
if (artistsSearchTimeout) {
clearTimeout(artistsSearchTimeout);
}
performArtistsSearch(query);
}
}
}
/**
* Perform artist search with API call
*/
async function performArtistsSearch(query) {
console.log(`🔍 Searching for artists: "${query}"`);
// Check cache first
if (artistsPageState.cache.searches[query]) {
console.log('📦 Using cached search results');
displayArtistsResults(query, artistsPageState.cache.searches[query]);
return;
}
// Update status
updateArtistsSearchStatus('searching');
// Show loading cards immediately if we're in results view
if (artistsPageState.currentView === 'results') {
showSearchLoadingCards();
}
try {
// Set up abort controller
artistsSearchController = new AbortController();
const response = await fetch('/api/match/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: query,
context: 'artist'
}),
signal: artistsSearchController.signal
});
if (!response.ok) {
throw new Error(`Search failed: ${response.status}`);
}
const data = await response.json();
console.log(`✅ Found ${data.results?.length || 0} artists`);
// Transform the results to flatten the nested artist data
const transformedResults = (data.results || []).map(result => {
// Extract artist data from the nested structure
const artist = result.artist || result;
return {
id: artist.id,
name: artist.name,
image_url: artist.image_url,
genres: artist.genres,
popularity: artist.popularity,
confidence: result.confidence || 0
};
});
console.log('🔧 Transformed results:', transformedResults);
// Cache the transformed results
artistsPageState.cache.searches[query] = transformedResults;
// Display results
displayArtistsResults(query, transformedResults);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('❌ Artist search failed:', error);
// Provide specific error messages based on the error type
let errorMessage = 'Search failed. Please try again.';
if (error.message.includes('401') || error.message.includes('authentication')) {
errorMessage = 'Spotify not authenticated. Please check your API settings.';
} else if (error.message.includes('network') || error.message.includes('fetch')) {
errorMessage = 'Network error. Please check your connection.';
} else if (error.message.includes('timeout')) {
errorMessage = 'Search timed out. Please try again.';
}
updateArtistsSearchStatus('error', errorMessage);
}
} finally {
artistsSearchController = null;
}
}
/**
* Display artist search results
*/
function displayArtistsResults(query, results) {
console.log(`📊 Displaying ${results.length} artist results`);
// Update state
artistsPageState.searchQuery = query;
artistsPageState.searchResults = results;
artistsPageState.currentView = 'results';
// Update header search input if different
const headerInput = document.getElementById('artists-header-search-input');
if (headerInput && headerInput.value !== query) {
headerInput.value = query;
}
// Show results state
showArtistsResultsState();
// Populate results
const container = document.getElementById('artists-cards-container');
if (!container) return;
if (results.length === 0) {
container.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 60px 20px; color: rgba(255, 255, 255, 0.6);">
<div style="font-size: 24px; margin-bottom: 12px;">🔍</div>
<div style="font-size: 16px; font-weight: 600; margin-bottom: 8px;">No artists found</div>
<div style="font-size: 14px;">Try a different search term</div>
</div>
`;
return;
}
// Create artist cards
container.innerHTML = results.map(result => createArtistCard(result)).join('');
// Add event listeners to cards
container.querySelectorAll('.artist-card').forEach((card, index) => {
card.addEventListener('click', () => selectArtist(results[index]));
});
// Add mouse wheel horizontal scrolling
container.addEventListener('wheel', (event) => {
if (event.deltaY !== 0) {
event.preventDefault();
container.scrollLeft += event.deltaY;
}
});
}
/**
* Create HTML for an artist card
*/
function createArtistCard(artist) {
const imageUrl = artist.image_url || '';
const genres = artist.genres && artist.genres.length > 0 ?
artist.genres.slice(0, 3).join(', ') : 'Various genres';
const popularity = artist.popularity || 0;
// Create a fallback gradient if no image is available
const backgroundStyle = imageUrl ?
`background-image: url('${imageUrl}');` :
`background: linear-gradient(135deg, rgba(29, 185, 84, 0.3) 0%, rgba(24, 156, 71, 0.2) 100%);`;
// Format popularity as a percentage for better UX
const popularityText = popularity > 0 ? `${popularity}% Popular` : 'Popularity Unknown';
return `
<div class="artist-card" data-artist-id="${artist.id}">
<div class="artist-card-background" style="${backgroundStyle}"></div>
<div class="artist-card-overlay"></div>
<div class="artist-card-content">
<div class="artist-card-name">${escapeHtml(artist.name)}</div>
<div class="artist-card-genres">${escapeHtml(genres)}</div>
<div class="artist-card-popularity">
<span class="popularity-icon">🔥</span>
<span>${popularityText}</span>
</div>
</div>
</div>
`;
}
/**
* Select an artist and show their discography
*/
async function selectArtist(artist) {
console.log(`🎤 Selected artist: ${artist.name}`);
// Update state
artistsPageState.selectedArtist = artist;
artistsPageState.currentView = 'detail';
// Show detail state
showArtistDetailState();
// Update artist info in header
updateArtistDetailHeader(artist);
// Load discography
await loadArtistDiscography(artist.id);
}
/**
* Load artist's discography from Spotify
*/
async function loadArtistDiscography(artistId) {
console.log(`💿 Loading discography for artist: ${artistId}`);
// Check cache first
if (artistsPageState.cache.discography[artistId]) {
console.log('📦 Using cached discography');
displayArtistDiscography(artistsPageState.cache.discography[artistId]);
return;
}
try {
// Show loading states
showDiscographyLoading();
// Call the real API endpoint
const response = await fetch(`/api/artist/${artistId}/discography`);
if (!response.ok) {
if (response.status === 401) {
throw new Error('Spotify not authenticated. Please check your API settings.');
}
throw new Error(`Failed to load discography: ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
const discography = {
albums: data.albums || [],
singles: data.singles || []
};
console.log(`✅ Loaded ${discography.albums.length} albums and ${discography.singles.length} singles`);
// Cache the results
artistsPageState.cache.discography[artistId] = discography;
artistsPageState.artistDiscography = discography;
// Display results
displayArtistDiscography(discography);
} catch (error) {
console.error('❌ Failed to load discography:', error);
showDiscographyError(error.message);
}
}
/**
* Display artist's discography in tabs
*/
function displayArtistDiscography(discography) {
console.log(`📀 Displaying discography: ${discography.albums?.length || 0} albums, ${discography.singles?.length || 0} singles`);
// Populate albums
const albumsContainer = document.getElementById('album-cards-container');
if (albumsContainer) {
if (discography.albums?.length > 0) {
albumsContainer.innerHTML = discography.albums.map(album => createAlbumCard(album)).join('');
} else {
albumsContainer.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px 20px; color: rgba(255, 255, 255, 0.6);">
<div style="font-size: 18px; margin-bottom: 8px;">💿</div>
<div style="font-size: 14px;">No albums found</div>
</div>
`;
}
}
// Populate singles
const singlesContainer = document.getElementById('singles-cards-container');
if (singlesContainer) {
if (discography.singles?.length > 0) {
singlesContainer.innerHTML = discography.singles.map(single => createAlbumCard(single)).join('');
} else {
singlesContainer.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px 20px; color: rgba(255, 255, 255, 0.6);">
<div style="font-size: 18px; margin-bottom: 8px;">🎵</div>
<div style="font-size: 14px;">No singles or EPs found</div>
</div>
`;
}
}
}
/**
* Create HTML for an album/single card
*/
function createAlbumCard(album) {
const imageUrl = album.image_url || '';
const year = album.release_date ? new Date(album.release_date).getFullYear() : '';
const type = album.album_type === 'album' ? 'Album' :
album.album_type === 'single' ? 'Single' : 'EP';
// Create a fallback gradient if no image is available
const backgroundStyle = imageUrl ?
`background-image: url('${imageUrl}');` :
`background: linear-gradient(135deg, rgba(29, 185, 84, 0.2) 0%, rgba(24, 156, 71, 0.1) 100%);`;
return `
<div class="album-card" data-album-id="${album.id}">
<div class="album-card-image" style="${backgroundStyle}"></div>
<div class="album-card-content">
<div class="album-card-name" title="${escapeHtml(album.name)}">${escapeHtml(album.name)}</div>
<div class="album-card-year">${year || 'Unknown'}</div>
<div class="album-card-type">${type}</div>
</div>
</div>
`;
}
/**
* Initialize artist detail tabs
*/
function initializeArtistTabs() {
const tabButtons = document.querySelectorAll('.artist-tab');
const tabContents = document.querySelectorAll('.tab-content');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const tabName = button.getAttribute('data-tab');
// Update button states
tabButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Update content states
tabContents.forEach(content => {
content.classList.remove('active');
if (content.id === `${tabName}-content`) {
content.classList.add('active');
}
});
console.log(`🔄 Switched to ${tabName} tab`);
});
});
}
/**
* State management functions
*/
function showArtistsSearchState() {
console.log('🔄 Showing search state');
const searchState = document.getElementById('artists-search-state');
const resultsState = document.getElementById('artists-results-state');
const detailState = document.getElementById('artist-detail-state');
if (searchState) {
searchState.classList.remove('hidden', 'fade-out');
}
if (resultsState) {
resultsState.classList.add('hidden');
resultsState.classList.remove('show');
}
if (detailState) {
detailState.classList.add('hidden');
detailState.classList.remove('show');
}
artistsPageState.currentView = 'search';
updateArtistsSearchStatus('default');
}
function showArtistsResultsState() {
console.log('🔄 Showing results state');
const searchState = document.getElementById('artists-search-state');
const resultsState = document.getElementById('artists-results-state');
const detailState = document.getElementById('artist-detail-state');
if (searchState) {
searchState.classList.add('fade-out');
setTimeout(() => searchState.classList.add('hidden'), 200);
}
if (resultsState) {
resultsState.classList.remove('hidden');
setTimeout(() => resultsState.classList.add('show'), 50);
}
if (detailState) {
detailState.classList.add('hidden');
detailState.classList.remove('show');
}
artistsPageState.currentView = 'results';
}
function showArtistDetailState() {
console.log('🔄 Showing detail state');
const searchState = document.getElementById('artists-search-state');
const resultsState = document.getElementById('artists-results-state');
const detailState = document.getElementById('artist-detail-state');
if (searchState) {
searchState.classList.add('hidden', 'fade-out');
}
if (resultsState) {
resultsState.classList.add('hidden');
resultsState.classList.remove('show');
}
if (detailState) {
detailState.classList.remove('hidden');
setTimeout(() => detailState.classList.add('show'), 50);
}
artistsPageState.currentView = 'detail';
}
/**
* Update search status text and styling
*/
function updateArtistsSearchStatus(status, message = null) {
const statusElement = document.getElementById('artists-search-status');
if (!statusElement) return;
// Clear all status classes
statusElement.classList.remove('searching', 'error');
switch (status) {
case 'default':
statusElement.textContent = 'Start typing to search for artists';
break;
case 'searching':
statusElement.classList.add('searching');
statusElement.textContent = 'Searching for artists...';
break;
case 'error':
statusElement.classList.add('error');
statusElement.innerHTML = `
<div style="margin-bottom: 8px;">${message || 'Search failed. Please try again.'}</div>
<button onclick="retryLastSearch()" style="
background: rgba(29, 185, 84, 0.15);
color: rgba(29, 185, 84, 0.9);
border: 1px solid rgba(29, 185, 84, 0.3);
border-radius: 8px;
padding: 4px 12px;
font-size: 12px;
cursor: pointer;
font-family: inherit;
" onmouseover="this.style.background='rgba(29, 185, 84, 0.25)'"
onmouseout="this.style.background='rgba(29, 185, 84, 0.15)'">
🔄 Retry Search
</button>
`;
break;
}
}
/**
* Retry the last search query
*/
function retryLastSearch() {
const searchInput = document.getElementById('artists-search-input');
const headerSearchInput = document.getElementById('artists-header-search-input');
// Get the last search query from either input
const query = searchInput?.value?.trim() || headerSearchInput?.value?.trim() || artistsPageState.searchQuery;
if (query) {
console.log(`🔄 Retrying search for: "${query}"`);
performArtistsSearch(query);
}
}
/**
* Update artist detail header with artist info
*/
function updateArtistDetailHeader(artist) {
const imageElement = document.getElementById('artist-detail-image');
const nameElement = document.getElementById('artist-detail-name');
const genresElement = document.getElementById('artist-detail-genres');
if (imageElement && artist.image_url) {
imageElement.style.backgroundImage = `url('${artist.image_url}')`;
}
if (nameElement) {
nameElement.textContent = artist.name;
}
if (genresElement) {
const genres = artist.genres?.slice(0, 4).join(' • ') || 'Various genres';
genresElement.textContent = genres;
}
}
/**
* Show loading state for discography
*/
function showDiscographyLoading() {
const albumsContainer = document.getElementById('album-cards-container');
const singlesContainer = document.getElementById('singles-cards-container');
const loadingHtml = `
<div class="album-card loading">
<div class="album-card-image"></div>
<div class="album-card-content">
<div class="album-card-name">Loading...</div>
<div class="album-card-year">-</div>
<div class="album-card-type">-</div>
</div>
</div>
`.repeat(4);
if (albumsContainer) albumsContainer.innerHTML = loadingHtml;
if (singlesContainer) singlesContainer.innerHTML = loadingHtml;
}
/**
* Show error state for discography
*/
function showDiscographyError(message = 'Failed to load discography') {
const albumsContainer = document.getElementById('album-cards-container');
const singlesContainer = document.getElementById('singles-cards-container');
const errorHtml = `
<div style="grid-column: 1 / -1; text-align: center; padding: 40px 20px; color: rgba(255, 65, 54, 0.8);">
<div style="font-size: 18px; margin-bottom: 8px;"></div>
<div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">Failed to load discography</div>
<div style="font-size: 12px; color: rgba(255, 65, 54, 0.6); max-width: 300px; margin: 0 auto;">${escapeHtml(message)}</div>
</div>
`;
if (albumsContainer) albumsContainer.innerHTML = errorHtml;
if (singlesContainer) singlesContainer.innerHTML = errorHtml;
}
/**
* Show loading cards while searching
*/
function showSearchLoadingCards() {
const container = document.getElementById('artists-cards-container');
if (!container) return;
const loadingCardHtml = `
<div class="artist-card loading">
<div class="artist-card-background"></div>
<div class="artist-card-overlay"></div>
<div class="artist-card-content">
<div class="artist-card-name">Loading...</div>
<div class="artist-card-genres">Fetching data...</div>
<div class="artist-card-popularity">
<span class="popularity-icon"></span>
<span>Loading...</span>
</div>
</div>
</div>
`;
// Show 6 loading cards
container.innerHTML = loadingCardHtml.repeat(6);
}
/**
* Utility function to escape HTML
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// --- Global Cleanup on Page Unload ---
// Note: Automatic wishlist processing now runs server-side and continues even when browser is closed

View file

@ -5628,4 +5628,758 @@ body {
0 8px 32px rgba(40, 167, 69, 0.15),
0 2px 8px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
/* ============================================================================ */
/* ARTISTS PAGE STYLES - ELEGANT GLASSMORPHIC DESIGN */
/* ============================================================================ */
/* Artists Search State - Initial centered interface */
.artists-search-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 70vh;
padding: 40px 20px;
opacity: 1;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.artists-search-state.fade-out {
opacity: 0;
transform: translateY(-20px);
}
.artists-search-container {
max-width: 600px;
width: 100%;
text-align: center;
}
.artists-welcome-section {
margin-bottom: 40px;
}
.artists-welcome-title {
font-size: 36px;
font-weight: 700;
color: #ffffff;
margin-bottom: 12px;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
}
.artists-welcome-subtitle {
font-size: 16px;
color: rgba(255, 255, 255, 0.7);
font-weight: 400;
line-height: 1.5;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
}
.artists-search-input-container {
position: relative;
margin-bottom: 20px;
}
.artists-search-input {
width: 100%;
padding: 18px 24px 18px 60px;
font-size: 16px;
font-weight: 500;
color: #ffffff;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
/* Premium glassmorphic foundation */
background: linear-gradient(135deg,
rgba(26, 26, 26, 0.9) 0%,
rgba(18, 18, 18, 0.95) 100%);
backdrop-filter: blur(12px) saturate(1.1);
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-top: 1px solid rgba(255, 255, 255, 0.15);
/* Elegant shadow system */
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(29, 185, 84, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.08),
inset 0 -1px 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
}
.artists-search-input:focus {
background: linear-gradient(135deg,
rgba(30, 30, 30, 0.95) 0%,
rgba(22, 22, 22, 1.0) 100%);
border-color: rgba(29, 185, 84, 0.3);
border-top-color: rgba(29, 185, 84, 0.4);
box-shadow:
0 12px 40px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(29, 185, 84, 0.15),
0 0 20px rgba(29, 185, 84, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.12),
inset 0 -1px 0 rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.artists-search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
font-weight: 400;
}
.artists-search-icon {
position: absolute;
left: 22px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
color: rgba(255, 255, 255, 0.6);
pointer-events: none;
transition: color 0.3s ease;
}
.artists-search-input:focus + .artists-search-icon {
color: rgba(29, 185, 84, 0.8);
}
.artists-search-status {
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
font-weight: 400;
min-height: 20px;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
transition: all 0.3s ease;
}
.artists-search-status.searching {
color: rgba(29, 185, 84, 0.8);
}
.artists-search-status.error {
color: rgba(255, 65, 54, 0.8);
}
/* Artists Results State */
.artists-results-state {
opacity: 0;
transform: translateY(20px);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
padding: 20px;
}
.artists-results-state.show {
opacity: 1;
transform: translateY(0);
}
.artists-results-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30px;
gap: 20px;
}
.artists-back-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.8);
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
}
.artists-back-button:hover {
background: rgba(255, 255, 255, 0.12);
color: #ffffff;
transform: translateY(-1px);
}
.back-icon {
font-size: 16px;
}
.artists-search-header {
flex: 1;
max-width: 400px;
}
.artists-header-search-input {
width: 100%;
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
color: #ffffff;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
transition: all 0.2s ease;
outline: none;
}
.artists-header-search-input:focus {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(29, 185, 84, 0.3);
box-shadow: 0 0 0 3px rgba(29, 185, 84, 0.1);
}
.artists-header-search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.artists-results-content {
margin-bottom: 40px;
}
.artists-results-title {
font-size: 20px;
font-weight: 600;
color: #ffffff;
margin-bottom: 20px;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
}
.artists-cards-container {
display: flex;
gap: 20px;
overflow-x: auto;
overflow-y: visible;
padding: 20px;
scroll-behavior: smooth;
/* Custom scrollbar styling */
scrollbar-width: thin;
scrollbar-color: rgba(29, 185, 84, 0.6) rgba(255, 255, 255, 0.1);
}
.artists-cards-container::-webkit-scrollbar {
height: 8px;
}
.artists-cards-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.artists-cards-container::-webkit-scrollbar-thumb {
background: rgba(29, 185, 84, 0.6);
border-radius: 4px;
transition: background 0.3s ease;
}
.artists-cards-container::-webkit-scrollbar-thumb:hover {
background: rgba(29, 185, 84, 0.8);
}
/* Artist Card Styles with Background Images */
.artist-card {
position: relative;
width: 280px;
height: 350px;
flex-shrink: 0;
border-radius: 20px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* Premium glassmorphic foundation */
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.08);
border-top: 1px solid rgba(255, 255, 255, 0.12);
/* Elegant shadow system */
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(29, 185, 84, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.artist-card:hover {
transform: translateY(-8px) scale(1.02);
z-index: 10;
box-shadow:
0 20px 50px rgba(0, 0, 0, 0.6),
0 0 0 1px rgba(29, 185, 84, 0.15),
0 0 30px rgba(29, 185, 84, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.artist-card-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: 0.6;
transition: all 0.3s ease;
}
.artist-card:hover .artist-card-background {
opacity: 0.8;
transform: scale(1.05);
}
.artist-card-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.9) 0%,
rgba(0, 0, 0, 0.3) 40%,
rgba(0, 0, 0, 0.1) 70%,
transparent 100%
);
}
.artist-card-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 30px 24px;
z-index: 2;
}
.artist-card-name {
font-size: 24px;
font-weight: 700;
color: #ffffff;
margin-bottom: 8px;
line-height: 1.2;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
}
.artist-card-genres {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
margin-bottom: 12px;
line-height: 1.3;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
}
.artist-card-popularity {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: rgba(29, 185, 84, 0.9);
font-weight: 600;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
}
.popularity-icon {
font-size: 12px;
}
/* Artist Card Animation States */
.artist-card.loading {
animation: artistCardPulse 2s ease-in-out infinite;
pointer-events: none;
}
.artist-card.loading .artist-card-background {
background: linear-gradient(
90deg,
rgba(26, 26, 26, 0.8) 0%,
rgba(40, 40, 40, 0.8) 50%,
rgba(26, 26, 26, 0.8) 100%
) !important;
background-size: 200% 100%;
animation: artistCardShimmer 1.5s ease-in-out infinite;
}
@keyframes artistCardPulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@keyframes artistCardShimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Search loading indicator */
.artists-search-status.searching::after {
content: '';
display: inline-block;
width: 12px;
height: 12px;
margin-left: 8px;
border: 2px solid rgba(29, 185, 84, 0.3);
border-radius: 50%;
border-top-color: rgba(29, 185, 84, 0.8);
animation: searchSpinner 0.8s ease-in-out infinite;
}
@keyframes searchSpinner {
to {
transform: rotate(360deg);
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.artists-cards-container {
gap: 16px;
padding: 15px 0 25px 0;
}
.artist-card {
width: 250px;
height: 300px;
}
.artist-card-name {
font-size: 20px;
}
}
/* Artist Detail State */
.artist-detail-state {
opacity: 0;
transform: translateY(20px);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
padding: 20px;
}
.artist-detail-state.show {
opacity: 1;
transform: translateY(0);
}
.artist-detail-header {
display: flex;
align-items: center;
gap: 30px;
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.artist-detail-back-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.8);
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
}
.artist-detail-back-button:hover {
background: rgba(255, 255, 255, 0.12);
color: #ffffff;
transform: translateY(-1px);
}
.artist-detail-info {
display: flex;
align-items: center;
gap: 24px;
flex: 1;
}
.artist-detail-image {
width: 80px;
height: 80px;
border-radius: 40px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border: 3px solid rgba(29, 185, 84, 0.3);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
/* Fallback gradient if no image */
background-image: linear-gradient(135deg,
rgba(29, 185, 84, 0.3) 0%,
rgba(24, 156, 71, 0.2) 100%);
}
.artist-detail-name {
font-size: 32px;
font-weight: 700;
color: #ffffff;
margin-bottom: 8px;
line-height: 1.2;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
}
.artist-detail-genres {
font-size: 16px;
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
}
.artist-detail-content {
margin-bottom: 40px;
}
.artist-detail-tabs {
display: flex;
gap: 8px;
margin-bottom: 30px;
padding: 6px;
background: rgba(255, 255, 255, 0.06);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.artist-tab {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 24px;
font-size: 14px;
font-weight: 600;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
background: transparent;
color: rgba(255, 255, 255, 0.6);
border: none;
border-radius: 16px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
}
.artist-tab.active {
background: linear-gradient(135deg,
rgba(29, 185, 84, 0.2) 0%,
rgba(24, 156, 71, 0.15) 100%);
color: rgba(29, 185, 84, 1.0);
box-shadow:
0 4px 12px rgba(29, 185, 84, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.artist-tab:not(.active):hover {
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.8);
transform: translateY(-1px);
}
.tab-icon {
font-size: 16px;
}
.artist-detail-discography {
min-height: 400px;
}
.tab-content {
display: none;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s ease;
}
.tab-content.active {
display: block;
opacity: 1;
transform: translateY(0);
}
.album-cards-container,
.singles-cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 24px;
}
/* Album Card Styles */
.album-card {
position: relative;
background: linear-gradient(135deg,
rgba(26, 26, 26, 0.95) 0%,
rgba(18, 18, 18, 0.98) 100%);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
overflow: hidden;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(29, 185, 84, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.album-card:hover {
transform: translateY(-6px) scale(1.03);
box-shadow:
0 16px 40px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(29, 185, 84, 0.15),
0 0 20px rgba(29, 185, 84, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.album-card-image {
width: 100%;
aspect-ratio: 1;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
/* Fallback gradient if no image */
background-image: linear-gradient(135deg,
rgba(29, 185, 84, 0.2) 0%,
rgba(24, 156, 71, 0.1) 100%);
}
.album-card-image::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
180deg,
transparent 0%,
transparent 60%,
rgba(0, 0, 0, 0.3) 100%
);
opacity: 0;
transition: opacity 0.3s ease;
}
.album-card:hover .album-card-image::after {
opacity: 1;
}
.album-card-content {
padding: 20px;
}
.album-card-name {
font-size: 16px;
font-weight: 700;
color: #ffffff;
margin-bottom: 6px;
line-height: 1.3;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
/* Text overflow handling */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.album-card-year {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
font-weight: 500;
margin-bottom: 8px;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
}
.album-card-type {
display: inline-block;
padding: 4px 10px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
background: rgba(29, 185, 84, 0.15);
color: rgba(29, 185, 84, 0.9);
border-radius: 8px;
border: 1px solid rgba(29, 185, 84, 0.2);
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
}
/* Loading states for album cards */
.album-card.loading .album-card-image {
background: linear-gradient(
90deg,
rgba(26, 26, 26, 0.8) 0%,
rgba(40, 40, 40, 0.8) 50%,
rgba(26, 26, 26, 0.8) 100%
);
background-size: 200% 100%;
animation: albumCardShimmer 1.5s ease-in-out infinite;
}
@keyframes albumCardShimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Responsive adjustments for detail view */
@media (max-width: 768px) {
.artist-detail-header {
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.artist-detail-info {
width: 100%;
}
.artist-detail-name {
font-size: 24px;
}
.album-cards-container,
.singles-cards-container {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 16px;
}
.artist-detail-tabs {
overflow-x: auto;
padding-bottom: 5px;
}
}