add to wishlist functionality for library page
This commit is contained in:
parent
ffdee23cb0
commit
09d633f80b
4 changed files with 1139 additions and 14 deletions
|
|
@ -6584,6 +6584,85 @@ def cleanup_wishlist():
|
|||
traceback.print_exc()
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/add-album-to-wishlist', methods=['POST'])
|
||||
def add_album_track_to_wishlist():
|
||||
"""Endpoint to add a single track from an album to the wishlist."""
|
||||
try:
|
||||
from core.wishlist_service import get_wishlist_service
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"success": False, "error": "No data provided"}), 400
|
||||
|
||||
track = data.get('track')
|
||||
artist = data.get('artist')
|
||||
album = data.get('album')
|
||||
source_type = data.get('source_type', 'album')
|
||||
source_context = data.get('source_context', {})
|
||||
|
||||
if not track or not artist or not album:
|
||||
return jsonify({"success": False, "error": "Missing required fields: track, artist, album"}), 400
|
||||
|
||||
# Create Spotify track data format expected by wishlist service
|
||||
spotify_track_data = {
|
||||
'id': track.get('id'),
|
||||
'name': track.get('name'),
|
||||
'artists': track.get('artists', []),
|
||||
'album': {
|
||||
'id': album.get('id'),
|
||||
'name': album.get('name'),
|
||||
'images': album.get('images', []) if 'images' in album else [{'url': album.get('image_url', '')}],
|
||||
'album_type': album.get('album_type', 'album'),
|
||||
'release_date': album.get('release_date', ''),
|
||||
'total_tracks': album.get('total_tracks', 1)
|
||||
},
|
||||
'duration_ms': track.get('duration_ms', 0),
|
||||
'track_number': track.get('track_number', 1),
|
||||
'explicit': track.get('explicit', False),
|
||||
'popularity': track.get('popularity', 0),
|
||||
'preview_url': track.get('preview_url'),
|
||||
'external_urls': track.get('external_urls', {})
|
||||
}
|
||||
|
||||
# Add source context information
|
||||
enhanced_source_context = {
|
||||
**source_context,
|
||||
'artist_id': artist.get('id'),
|
||||
'artist_name': artist.get('name'),
|
||||
'album_id': album.get('id'),
|
||||
'album_name': album.get('name'),
|
||||
'added_via': 'library_wishlist_modal'
|
||||
}
|
||||
|
||||
# Get wishlist service and add track
|
||||
wishlist_service = get_wishlist_service()
|
||||
|
||||
success = wishlist_service.add_spotify_track_to_wishlist(
|
||||
spotify_track_data=spotify_track_data,
|
||||
failure_reason="Added from library (incomplete album)",
|
||||
source_type=source_type,
|
||||
source_context=enhanced_source_context
|
||||
)
|
||||
|
||||
if success:
|
||||
print(f"✅ Added track '{track.get('name')}' by '{artist.get('name')}' to wishlist")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"Added '{track.get('name')}' to wishlist"
|
||||
})
|
||||
else:
|
||||
print(f"❌ Failed to add track '{track.get('name')}' to wishlist")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Failed to add track to wishlist"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error adding track to wishlist: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/database/update', methods=['POST'])
|
||||
def start_database_update():
|
||||
"""Endpoint to start the database update process."""
|
||||
|
|
|
|||
|
|
@ -1241,7 +1241,47 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Add to Wishlist Modal -->
|
||||
<div class="modal-overlay hidden" id="add-to-wishlist-modal-overlay">
|
||||
<div class="add-to-wishlist-modal" id="add-to-wishlist-modal">
|
||||
<div class="add-to-wishlist-modal-header">
|
||||
<div class="add-to-wishlist-modal-hero" id="add-to-wishlist-modal-hero">
|
||||
<!-- Hero content will be dynamically populated -->
|
||||
</div>
|
||||
<div class="add-to-wishlist-modal-header-actions">
|
||||
<span class="add-to-wishlist-modal-close" onclick="closeAddToWishlistModal()">×</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="add-to-wishlist-modal-content">
|
||||
<div class="add-to-wishlist-modal-body">
|
||||
<div class="wishlist-track-list-container">
|
||||
<div class="wishlist-track-list-header">
|
||||
<h3>Tracks to Add to Wishlist</h3>
|
||||
<p class="wishlist-track-list-subtitle">All tracks from this release will be added to your wishlist</p>
|
||||
</div>
|
||||
|
||||
<div class="wishlist-track-list" id="wishlist-track-list">
|
||||
<!-- Track list will be dynamically populated -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="add-to-wishlist-modal-footer">
|
||||
<div class="wishlist-modal-actions">
|
||||
<button class="wishlist-modal-btn wishlist-modal-btn-secondary" onclick="closeAddToWishlistModal()">
|
||||
Close
|
||||
</button>
|
||||
<button class="wishlist-modal-btn wishlist-modal-btn-primary" id="confirm-add-to-wishlist-btn">
|
||||
Add to Wishlist
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -6297,6 +6297,300 @@ function getSuccessfulDownloadCount(process) {
|
|||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// ADD TO WISHLIST MODAL FUNCTIONS
|
||||
// ===============================
|
||||
|
||||
let currentWishlistModalData = null;
|
||||
|
||||
/**
|
||||
* Open the Add to Wishlist modal for an album/EP/single
|
||||
* @param {Object} album - Album object with id, name, image_url, etc.
|
||||
* @param {Object} artist - Artist object with id, name, image_url
|
||||
* @param {Array} tracks - Array of track objects
|
||||
* @param {string} albumType - Type of release (album, EP, single)
|
||||
*/
|
||||
async function openAddToWishlistModal(album, artist, tracks, albumType) {
|
||||
console.log(`🎵 Opening Add to Wishlist modal for: ${artist.name} - ${album.name}`);
|
||||
|
||||
try {
|
||||
// Store current modal data for use by other functions
|
||||
currentWishlistModalData = {
|
||||
album,
|
||||
artist,
|
||||
tracks,
|
||||
albumType
|
||||
};
|
||||
|
||||
const modal = document.getElementById('add-to-wishlist-modal');
|
||||
const overlay = document.getElementById('add-to-wishlist-modal-overlay');
|
||||
|
||||
if (!modal || !overlay) {
|
||||
console.error('Add to wishlist modal elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate and populate hero section
|
||||
const heroContent = generateWishlistModalHeroSection(album, artist, tracks, albumType);
|
||||
const heroContainer = document.getElementById('add-to-wishlist-modal-hero');
|
||||
if (heroContainer) {
|
||||
heroContainer.innerHTML = heroContent;
|
||||
}
|
||||
|
||||
// Generate and populate track list
|
||||
const trackListHTML = generateWishlistTrackList(tracks);
|
||||
const trackListContainer = document.getElementById('wishlist-track-list');
|
||||
if (trackListContainer) {
|
||||
trackListContainer.innerHTML = trackListHTML;
|
||||
}
|
||||
|
||||
// Set up the "Add to Wishlist" button click handler
|
||||
const addToWishlistBtn = document.getElementById('confirm-add-to-wishlist-btn');
|
||||
if (addToWishlistBtn) {
|
||||
addToWishlistBtn.onclick = () => handleAddToWishlist();
|
||||
}
|
||||
|
||||
// Show the modal
|
||||
overlay.classList.remove('hidden');
|
||||
|
||||
console.log(`✅ Successfully opened Add to Wishlist modal for: ${album.name}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error opening Add to Wishlist modal:', error);
|
||||
showToast(`Error opening wishlist modal: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the hero section HTML for the wishlist modal
|
||||
*/
|
||||
function generateWishlistModalHeroSection(album, artist, tracks, albumType) {
|
||||
const artistImage = artist.image_url || '';
|
||||
const albumImage = album.image_url || '';
|
||||
const trackCount = tracks.length;
|
||||
|
||||
let heroBackgroundImage = '';
|
||||
if (albumImage) {
|
||||
heroBackgroundImage = `<div class="add-to-wishlist-modal-hero-bg" style="background-image: url('${albumImage}');"></div>`;
|
||||
}
|
||||
|
||||
const heroContent = `
|
||||
<div class="add-to-wishlist-modal-hero-content">
|
||||
<div class="add-to-wishlist-modal-hero-images">
|
||||
${artistImage ? `<img class="add-to-wishlist-modal-hero-image artist" src="${artistImage}" alt="${escapeHtml(artist.name)}">` : ''}
|
||||
${albumImage ? `<img class="add-to-wishlist-modal-hero-image album" src="${albumImage}" alt="${escapeHtml(album.name)}">` : ''}
|
||||
</div>
|
||||
<div class="add-to-wishlist-modal-hero-metadata">
|
||||
<h1 class="add-to-wishlist-modal-hero-title">${escapeHtml(album.name || 'Unknown Album')}</h1>
|
||||
<div class="add-to-wishlist-modal-hero-subtitle">by ${escapeHtml(artist.name || 'Unknown Artist')}</div>
|
||||
<div class="add-to-wishlist-modal-hero-details">
|
||||
<span class="add-to-wishlist-modal-hero-detail">${albumType || 'Album'}</span>
|
||||
<span class="add-to-wishlist-modal-hero-detail">${trackCount} track${trackCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
${heroBackgroundImage}
|
||||
${heroContent}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the track list HTML for the wishlist modal
|
||||
*/
|
||||
function generateWishlistTrackList(tracks) {
|
||||
if (!tracks || tracks.length === 0) {
|
||||
return '<div style="text-align: center; padding: 40px; color: rgba(255, 255, 255, 0.6);">No tracks found</div>';
|
||||
}
|
||||
|
||||
return tracks.map((track, index) => {
|
||||
const trackNumber = track.track_number || (index + 1);
|
||||
const trackName = escapeHtml(track.name || 'Unknown Track');
|
||||
const artistsString = formatArtists(track.artists) || 'Unknown Artist';
|
||||
const duration = formatDuration(track.duration_ms);
|
||||
|
||||
return `
|
||||
<div class="wishlist-track-item">
|
||||
<div class="wishlist-track-number">${trackNumber}</div>
|
||||
<div class="wishlist-track-info">
|
||||
<div class="wishlist-track-name">${trackName}</div>
|
||||
<div class="wishlist-track-artists">${artistsString}</div>
|
||||
</div>
|
||||
<div class="wishlist-track-duration">${duration}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the "Add to Wishlist" button click
|
||||
*/
|
||||
async function handleAddToWishlist() {
|
||||
if (!currentWishlistModalData) {
|
||||
console.error('❌ No wishlist modal data available');
|
||||
return;
|
||||
}
|
||||
|
||||
const { album, artist, tracks, albumType } = currentWishlistModalData;
|
||||
const addToWishlistBtn = document.getElementById('confirm-add-to-wishlist-btn');
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
if (addToWishlistBtn) {
|
||||
addToWishlistBtn.classList.add('loading');
|
||||
addToWishlistBtn.textContent = 'Adding...';
|
||||
addToWishlistBtn.disabled = true;
|
||||
}
|
||||
|
||||
console.log(`🔄 Adding ${tracks.length} tracks to wishlist for: ${artist.name} - ${album.name}`);
|
||||
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// Add each track to wishlist individually
|
||||
for (const track of tracks) {
|
||||
try {
|
||||
// Ensure artists field is in the correct format (array of objects)
|
||||
let formattedArtists = track.artists;
|
||||
if (typeof track.artists === 'string') {
|
||||
// If artists is a string, convert to array of objects
|
||||
formattedArtists = [{ name: track.artists }];
|
||||
} else if (Array.isArray(track.artists)) {
|
||||
// If artists is already an array, ensure each item is an object
|
||||
formattedArtists = track.artists.map(artistItem => {
|
||||
if (typeof artistItem === 'string') {
|
||||
return { name: artistItem };
|
||||
} else if (typeof artistItem === 'object' && artistItem !== null) {
|
||||
return artistItem;
|
||||
} else {
|
||||
return { name: 'Unknown Artist' };
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fallback to array with single artist object
|
||||
formattedArtists = [{ name: artist.name }];
|
||||
}
|
||||
|
||||
const formattedTrack = {
|
||||
...track,
|
||||
artists: formattedArtists
|
||||
};
|
||||
|
||||
console.log(`🔄 Adding track with formatted artists:`, formattedTrack.name, formattedTrack.artists);
|
||||
|
||||
const response = await fetch('/api/add-album-to-wishlist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
track: formattedTrack,
|
||||
artist: artist,
|
||||
album: album,
|
||||
source_type: 'album',
|
||||
source_context: {
|
||||
album_name: album.name,
|
||||
artist_name: artist.name,
|
||||
album_type: albumType
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
successCount++;
|
||||
console.log(`✅ Added "${track.name}" to wishlist`);
|
||||
} else {
|
||||
errorCount++;
|
||||
console.error(`❌ Failed to add "${track.name}" to wishlist: ${result.error}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
console.error(`❌ Error adding "${track.name}" to wishlist:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Show completion message
|
||||
if (successCount > 0) {
|
||||
const message = errorCount > 0
|
||||
? `Added ${successCount}/${tracks.length} tracks to wishlist (${errorCount} failed)`
|
||||
: `Added ${successCount} tracks to wishlist`;
|
||||
showToast(message, successCount === tracks.length ? 'success' : 'warning');
|
||||
} else {
|
||||
showToast('Failed to add any tracks to wishlist', 'error');
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
closeAddToWishlistModal();
|
||||
|
||||
console.log(`✅ Wishlist addition complete: ${successCount} successful, ${errorCount} failed`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error in handleAddToWishlist:', error);
|
||||
showToast(`Error adding to wishlist: ${error.message}`, 'error');
|
||||
} finally {
|
||||
// Reset button state
|
||||
if (addToWishlistBtn) {
|
||||
addToWishlistBtn.classList.remove('loading');
|
||||
addToWishlistBtn.textContent = 'Add to Wishlist';
|
||||
addToWishlistBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the Add to Wishlist modal
|
||||
*/
|
||||
function closeAddToWishlistModal() {
|
||||
console.log('🔄 Closing Add to Wishlist modal');
|
||||
|
||||
try {
|
||||
const overlay = document.getElementById('add-to-wishlist-modal-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Clear current modal data
|
||||
currentWishlistModalData = null;
|
||||
|
||||
// Clear hero content
|
||||
const heroContainer = document.getElementById('add-to-wishlist-modal-hero');
|
||||
if (heroContainer) {
|
||||
heroContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// Clear track list
|
||||
const trackListContainer = document.getElementById('wishlist-track-list');
|
||||
if (trackListContainer) {
|
||||
trackListContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
console.log('✅ Add to Wishlist modal closed successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error closing Add to Wishlist modal:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration from milliseconds to MM:SS format
|
||||
*/
|
||||
function formatDuration(durationMs) {
|
||||
if (!durationMs || durationMs <= 0) {
|
||||
return '--:--';
|
||||
}
|
||||
|
||||
const totalSeconds = Math.floor(durationMs / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Download Missing Tracks Modal functions
|
||||
window.openDownloadMissingModal = openDownloadMissingModal;
|
||||
window.closeDownloadMissingModal = closeDownloadMissingModal;
|
||||
|
|
@ -6306,11 +6600,16 @@ window.cancelTrackDownload = cancelTrackDownload; // Legacy system
|
|||
window.cancelTrackDownloadV2 = cancelTrackDownloadV2; // NEW V2 system
|
||||
window.handleViewProgressClick = handleViewProgressClick;
|
||||
|
||||
// Wishlist Modal functions
|
||||
// Wishlist Modal functions (existing)
|
||||
window.openDownloadMissingWishlistModal = openDownloadMissingWishlistModal;
|
||||
window.startWishlistMissingTracksProcess = startWishlistMissingTracksProcess;
|
||||
window.handleWishlistButtonClick = handleWishlistButtonClick;
|
||||
|
||||
// Add to Wishlist Modal functions (new)
|
||||
window.openAddToWishlistModal = openAddToWishlistModal;
|
||||
window.closeAddToWishlistModal = closeAddToWishlistModal;
|
||||
window.handleAddToWishlist = handleAddToWishlist;
|
||||
|
||||
// Helper functions
|
||||
window.escapeHtml = escapeHtml;
|
||||
window.formatArtists = formatArtists;
|
||||
|
|
@ -11536,22 +11835,109 @@ function showSearchLoadingCards() {
|
|||
// ARTIST ALBUM DOWNLOAD MISSING TRACKS INTEGRATION
|
||||
// ===============================
|
||||
|
||||
/**
|
||||
* Get the completion status of an album from cached data or DOM
|
||||
* @param {string} albumId - The album ID
|
||||
* @param {string} albumType - The album type ('albums' or 'singles')
|
||||
* @returns {Object|null} - Completion status object or null
|
||||
*/
|
||||
function getAlbumCompletionStatus(albumId, albumType) {
|
||||
try {
|
||||
// First, check cached completion data
|
||||
const artistId = artistsPageState.selectedArtist?.id;
|
||||
if (artistId && artistsPageState.cache.completionData[artistId]) {
|
||||
const cachedData = artistsPageState.cache.completionData[artistId];
|
||||
const dataArray = albumType === 'albums' ? cachedData.albums : cachedData.singles;
|
||||
|
||||
if (dataArray) {
|
||||
const completionData = dataArray.find(item => item.album_id === albumId || item.id === albumId);
|
||||
if (completionData) {
|
||||
console.log(`📊 Found cached completion data for album ${albumId}:`, completionData);
|
||||
return completionData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Check DOM completion overlay
|
||||
const containerId = albumType === 'albums' ? 'album-cards-container' : 'singles-cards-container';
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
if (container) {
|
||||
const albumCard = container.querySelector(`[data-album-id="${albumId}"]`);
|
||||
if (albumCard) {
|
||||
const overlay = albumCard.querySelector('.completion-overlay');
|
||||
if (overlay) {
|
||||
// Extract status from overlay classes
|
||||
const classList = Array.from(overlay.classList);
|
||||
const statusClasses = ['completed', 'nearly_complete', 'partial', 'missing', 'downloading', 'downloaded', 'error'];
|
||||
const status = statusClasses.find(cls => classList.includes(cls));
|
||||
|
||||
if (status) {
|
||||
console.log(`📊 Found DOM completion status for album ${albumId}: ${status}`);
|
||||
return { status, completion_percentage: status === 'completed' ? 100 : 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`⚠️ No completion status found for album ${albumId}`);
|
||||
return null;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error getting album completion status for ${albumId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle album/single/EP click to open download missing tracks modal
|
||||
*/
|
||||
async function handleArtistAlbumClick(album, albumType) {
|
||||
console.log(`🎵 Album clicked: ${album.name} (${album.album_type}) from artist: ${artistsPageState.selectedArtist?.name}`);
|
||||
|
||||
|
||||
if (!artistsPageState.selectedArtist) {
|
||||
console.error('❌ No selected artist found');
|
||||
showToast('Error: No artist selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Check completion status of the album
|
||||
const completionStatus = getAlbumCompletionStatus(album.id, albumType);
|
||||
console.log(`📊 Album completion status: ${completionStatus?.status || 'unknown'} (${completionStatus?.completion_percentage || 0}%)`);
|
||||
|
||||
// If album is complete, show informational message and exit
|
||||
if (completionStatus?.status === 'completed') {
|
||||
showToast(`${album.name} is already complete in your library`, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
// If album has missing tracks, show Add to Wishlist modal
|
||||
if (completionStatus?.status && ['partial', 'missing', 'nearly_complete'].includes(completionStatus.status)) {
|
||||
console.log(`🎵 Album has missing tracks, opening Add to Wishlist modal`);
|
||||
|
||||
// Load tracks for the album
|
||||
const response = await fetch(`/api/artist/${artistsPageState.selectedArtist.id}/album/${album.id}/tracks`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load album tracks: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
||||
throw new Error('No tracks found for this album');
|
||||
}
|
||||
|
||||
// Open the Add to Wishlist modal
|
||||
await openAddToWishlistModal(album, artistsPageState.selectedArtist, data.tracks, albumType);
|
||||
return;
|
||||
}
|
||||
|
||||
// For unknown status or if we want to check what's missing, use the existing download modal
|
||||
console.log(`🔄 Opening existing download missing tracks modal for status analysis`);
|
||||
|
||||
// Create virtual playlist ID
|
||||
const virtualPlaylistId = `artist_album_${artistsPageState.selectedArtist.id}_${album.id}`;
|
||||
|
||||
|
||||
// Check if modal already exists and show it
|
||||
if (activeDownloadProcesses[virtualPlaylistId]) {
|
||||
console.log(`📱 Reopening existing modal for ${album.name}`);
|
||||
|
|
@ -11564,12 +11950,12 @@ async function handleArtistAlbumClick(album, albumType) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create virtual playlist and open modal
|
||||
await createArtistAlbumVirtualPlaylist(album, albumType);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error opening download missing tracks modal:', error);
|
||||
console.error('❌ Error handling album click:', error);
|
||||
showToast(`Error opening download modal: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
|
@ -14373,19 +14759,101 @@ function createReleaseCard(release) {
|
|||
card.appendChild(year);
|
||||
card.appendChild(completion);
|
||||
|
||||
// Add click handler for future functionality
|
||||
card.addEventListener("click", () => {
|
||||
// Add click handler for release card
|
||||
card.addEventListener("click", async () => {
|
||||
console.log(`Clicked on release: ${release.title} (Owned: ${release.owned})`);
|
||||
if (release.owned) {
|
||||
showToast(`Album details coming soon!`, "info");
|
||||
} else {
|
||||
showToast(`Add to download queue coming soon!`, "info");
|
||||
|
||||
// For owned/complete releases, show info message
|
||||
if (release.owned && (!release.track_completion ||
|
||||
(typeof release.track_completion === 'object' && release.track_completion.missing_tracks === 0) ||
|
||||
(typeof release.track_completion === 'number' && release.track_completion === 100))) {
|
||||
showToast(`${release.title} is already complete in your library`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
// For missing or incomplete releases, open wishlist modal
|
||||
try {
|
||||
// Convert release object to album format expected by our function
|
||||
const albumData = {
|
||||
id: release.spotify_id || release.id,
|
||||
name: release.title,
|
||||
image_url: release.image_url,
|
||||
release_date: release.year ? `${release.year}-01-01` : '',
|
||||
album_type: release.type || 'album',
|
||||
total_tracks: (release.track_completion && typeof release.track_completion === 'object')
|
||||
? release.track_completion.total_tracks : 1
|
||||
};
|
||||
|
||||
// Get current artist from artist detail page state
|
||||
const currentArtist = artistDetailPageState.currentArtistName ? {
|
||||
id: artistDetailPageState.currentArtistId,
|
||||
name: artistDetailPageState.currentArtistName,
|
||||
image_url: getArtistImageFromPage() || '' // Get artist image from page
|
||||
} : null;
|
||||
|
||||
if (!currentArtist) {
|
||||
console.error('❌ No current artist found for release click');
|
||||
showToast('Error: No artist information available', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load tracks for the album
|
||||
const response = await fetch(`/api/artist/${currentArtist.id}/album/${albumData.id}/tracks`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load album tracks: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
||||
throw new Error('No tracks found for this release');
|
||||
}
|
||||
|
||||
// Determine album type based on release data
|
||||
const albumType = release.type === 'single' ? 'singles' : 'albums';
|
||||
|
||||
// Open the Add to Wishlist modal
|
||||
await openAddToWishlistModal(albumData, currentArtist, data.tracks, albumType);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error handling release click:', error);
|
||||
showToast(`Error opening wishlist modal: ${error.message}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get artist image from the current artist detail page
|
||||
*/
|
||||
function getArtistImageFromPage() {
|
||||
try {
|
||||
// Try to get from artist detail image element
|
||||
const artistDetailImage = document.getElementById('artist-detail-image');
|
||||
if (artistDetailImage && artistDetailImage.src && artistDetailImage.src !== window.location.href) {
|
||||
return artistDetailImage.src;
|
||||
}
|
||||
|
||||
// Try to get from artist hero image
|
||||
const artistImage = document.getElementById('artist-image');
|
||||
if (artistImage) {
|
||||
const bgImage = window.getComputedStyle(artistImage).backgroundImage;
|
||||
if (bgImage && bgImage !== 'none') {
|
||||
// Extract URL from CSS background-image
|
||||
const urlMatch = bgImage.match(/url\(["']?(.*?)["']?\)/);
|
||||
if (urlMatch && urlMatch[1]) {
|
||||
return urlMatch[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn('Error getting artist image from page:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// UI state management functions
|
||||
function showArtistDetailLoading(show) {
|
||||
const loadingElement = document.getElementById("artist-detail-loading");
|
||||
|
|
|
|||
|
|
@ -9543,3 +9543,541 @@ body {
|
|||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
ADD TO WISHLIST MODAL STYLES
|
||||
================================================ */
|
||||
|
||||
/* Modal Overlay and Container */
|
||||
.add-to-wishlist-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 900px;
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
/* Premium glassmorphic foundation matching download modal */
|
||||
background: linear-gradient(135deg,
|
||||
rgba(20, 20, 20, 0.95) 0%,
|
||||
rgba(12, 12, 12, 0.98) 100%);
|
||||
backdrop-filter: blur(20px) saturate(1.2);
|
||||
|
||||
/* Enhanced borders */
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
|
||||
/* Premium shadow effect matching download modal */
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.6),
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 0 40px rgba(29, 185, 84, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
#add-to-wishlist-modal-overlay:not(.hidden) .add-to-wishlist-modal {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Modal Header */
|
||||
.add-to-wishlist-modal-header {
|
||||
/* Enhanced gradient with subtle transparency and glow */
|
||||
background: linear-gradient(135deg,
|
||||
rgba(45, 45, 45, 0.8) 0%,
|
||||
rgba(26, 26, 26, 0.9) 100%);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-top-left-radius: 20px;
|
||||
border-top-right-radius: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.add-to-wishlist-modal-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 28px;
|
||||
gap: 20px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 0.3;
|
||||
filter: blur(20px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-bg::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(26, 26, 26, 0.7) 0%,
|
||||
rgba(18, 18, 18, 0.8) 100%);
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-images {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-image {
|
||||
border-radius: 12px;
|
||||
object-fit: cover;
|
||||
box-shadow:
|
||||
0 8px 24px rgba(0, 0, 0, 0.4),
|
||||
0 4px 12px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-image:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow:
|
||||
0 12px 32px rgba(0, 0, 0, 0.5),
|
||||
0 6px 16px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-image.artist {
|
||||
border-radius: 50%;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-image.album {
|
||||
border-radius: 12px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
rgba(255, 255, 255, 0.05) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-metadata {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-title {
|
||||
color: #ffffff;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-subtitle {
|
||||
color: #e0e0e0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-details {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-detail {
|
||||
color: #1ed760;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
background: rgba(29, 185, 84, 0.1);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(29, 185, 84, 0.2);
|
||||
box-shadow: 0 2px 6px rgba(29, 185, 84, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Header Actions */
|
||||
.add-to-wishlist-modal-header-actions {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 24px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-close {
|
||||
color: #b3b3b3;
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-close:hover {
|
||||
color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Modal Content */
|
||||
.add-to-wishlist-modal-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-body {
|
||||
flex: 1;
|
||||
padding: 24px 28px;
|
||||
overflow-y: auto;
|
||||
background: #121212;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-body::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-body::-webkit-scrollbar-thumb {
|
||||
background: rgba(29, 185, 84, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-body::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(29, 185, 84, 0.5);
|
||||
}
|
||||
|
||||
/* Track List Container */
|
||||
.wishlist-track-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.wishlist-track-list-header {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.wishlist-track-list-header h3 {
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.wishlist-track-list-subtitle {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Track List */
|
||||
.wishlist-track-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.wishlist-track-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.wishlist-track-list::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.wishlist-track-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(29, 185, 84, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.wishlist-track-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.05) 0%,
|
||||
rgba(255, 255, 255, 0.02) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wishlist-track-item:hover {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(29, 185, 84, 0.08) 0%,
|
||||
rgba(29, 185, 84, 0.03) 100%);
|
||||
border-color: rgba(29, 185, 84, 0.2);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.wishlist-track-number {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wishlist-track-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wishlist-track-name {
|
||||
color: #ffffff;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wishlist-track-artists {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wishlist-track-duration {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Modal Footer */
|
||||
.add-to-wishlist-modal-footer {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(42, 42, 42, 0.9) 0%,
|
||||
rgba(30, 30, 30, 0.95) 100%);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding: 24px 28px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
|
||||
.wishlist-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wishlist-modal-btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.wishlist-modal-btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.wishlist-modal-btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.wishlist-modal-btn-primary {
|
||||
background: linear-gradient(135deg, #1db954 0%, #1ed760 100%);
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(29, 185, 84, 0.3);
|
||||
}
|
||||
|
||||
.wishlist-modal-btn-primary:hover {
|
||||
background: linear-gradient(135deg, #1ed760 0%, #22e167 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 24px rgba(29, 185, 84, 0.4);
|
||||
}
|
||||
|
||||
.wishlist-modal-btn-primary:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 4px 12px rgba(29, 185, 84, 0.3);
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.wishlist-modal-btn-primary.loading {
|
||||
background: rgba(255, 193, 7, 0.9);
|
||||
color: #000000;
|
||||
animation: pulse-loading 1.5s infinite;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wishlist-modal-btn-primary:disabled {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Animation for modal appearance */
|
||||
@keyframes wishlistModalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#add-to-wishlist-modal-overlay:not(.hidden) .add-to-wishlist-modal {
|
||||
animation: wishlistModalFadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* Mobile Responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.add-to-wishlist-modal {
|
||||
width: 95vw;
|
||||
max-height: 90vh;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
min-height: auto;
|
||||
padding: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-content {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-images {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-image.album,
|
||||
.add-to-wishlist-modal-hero-image.artist {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 28px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-title {
|
||||
font-size: 18px;
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-subtitle {
|
||||
font-size: 14px;
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-hero-details {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.add-to-wishlist-modal-footer {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.wishlist-modal-actions {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wishlist-modal-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue