artists page
This commit is contained in:
parent
57b0833fe2
commit
c29c5d6495
4 changed files with 1613 additions and 6 deletions
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue