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})
|
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'])
|
@app.route('/api/stream/start', methods=['POST'])
|
||||||
def stream_start():
|
def stream_start():
|
||||||
"""Start streaming a track in the background"""
|
"""Start streaming a track in the background"""
|
||||||
|
|
|
||||||
|
|
@ -528,12 +528,94 @@
|
||||||
|
|
||||||
<!-- Artists Page -->
|
<!-- Artists Page -->
|
||||||
<div class="page" id="artists-page">
|
<div class="page" id="artists-page">
|
||||||
<div class="page-header">
|
<!-- Initial Search State -->
|
||||||
<h2>Artists</h2>
|
<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>
|
||||||
<div class="artists-content">
|
|
||||||
<div class="artists-grid" id="artists-grid">
|
<!-- Search Results State -->
|
||||||
<!-- Artists will be populated here -->
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,24 @@ let tidalPlaylists = [];
|
||||||
let tidalPlaylistStates = {}; // Key: playlist_id, Value: playlist state with phases
|
let tidalPlaylistStates = {}; // Key: playlist_id, Value: playlist state with phases
|
||||||
let tidalPlaylistsLoaded = false;
|
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 ---
|
// --- Wishlist Modal Persistence State Management ---
|
||||||
const WishlistModalState = {
|
const WishlistModalState = {
|
||||||
// Track if wishlist modal was visible before page refresh
|
// Track if wishlist modal was visible before page refresh
|
||||||
|
|
@ -343,7 +361,7 @@ async function loadPageData(pageId) {
|
||||||
break;
|
break;
|
||||||
case 'artists':
|
case 'artists':
|
||||||
stopDownloadPolling();
|
stopDownloadPolling();
|
||||||
await loadArtistsData();
|
initializeArtistsPage();
|
||||||
break;
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
initializeSettings();
|
initializeSettings();
|
||||||
|
|
@ -5183,6 +5201,7 @@ window.selectAlbum = selectAlbum;
|
||||||
window.navigateToPage = navigateToPage;
|
window.navigateToPage = navigateToPage;
|
||||||
window.openKofi = openKofi;
|
window.openKofi = openKofi;
|
||||||
window.copyAddress = copyAddress;
|
window.copyAddress = copyAddress;
|
||||||
|
window.retryLastSearch = retryLastSearch;
|
||||||
window.showVersionInfo = showVersionInfo;
|
window.showVersionInfo = showVersionInfo;
|
||||||
window.closeVersionModal = closeVersionModal;
|
window.closeVersionModal = closeVersionModal;
|
||||||
window.testConnection = testConnection;
|
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 ---
|
// --- Global Cleanup on Page Unload ---
|
||||||
// Note: Automatic wishlist processing now runs server-side and continues even when browser is closed
|
// 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 8px 32px rgba(40, 167, 69, 0.15),
|
||||||
0 2px 8px rgba(0, 0, 0, 0.4),
|
0 2px 8px rgba(0, 0, 0, 0.4),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
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