import { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { Sparkles, Play, Plus, Music, Heart, Shuffle, Download, ExternalLink, Trash2, Wand2 } from 'lucide-react'; import { useStore } from '../store/useStore'; import { generateMixedPlaylist, createPlaylist, addTracksToPlaylist } from '../utils/spotify'; import { apiPost, apiGet, apiDelete, API_BASE } from '../utils/api'; import { formatDuration } from '../utils/cn'; import { getThemeClasses } from '../utils/theme'; import toast from 'react-hot-toast'; // PlaylistImage component to handle image loading with fallback const PlaylistImage = ({ imageUrl, alt, fallbackGradient }: { imageUrl: string; alt: string; fallbackGradient: string }) => { const [src, setSrc] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); useEffect(() => { if (imageUrl.startsWith('http')) { // Direct HTTP/HTTPS URL setSrc(imageUrl); setLoading(false); } else { // API endpoint - fetch as base64 via same-protocol API_BASE to avoid mixed content const fetchImage = async () => { try { const response = await fetch(`${API_BASE}${imageUrl}`); if (response.ok) { const data = await response.json(); setSrc(data.dataUrl); } else { setError(true); } } catch (e) { setError(true); } finally { setLoading(false); } }; fetchImage(); } }, [imageUrl]); if (loading) { return (
); } if (error || !src) { return (
); } return ( {alt} setError(true)} /> ); }; // Generate beautiful gradients based on playlist vibe and genres function getPlaylistGradient(playlist: any): string { const vibe = playlist.vibe?.toLowerCase(); const genres = playlist.genres || []; // Vibe-based gradients if (vibe) { switch (vibe) { case 'energetic': return 'bg-gradient-to-br from-red-500 to-orange-500'; case 'chill': return 'bg-gradient-to-br from-blue-500 to-teal-500'; case 'romantic': return 'bg-gradient-to-br from-pink-500 to-rose-500'; case 'party': return 'bg-gradient-to-br from-purple-500 to-pink-500'; case 'workout': return 'bg-gradient-to-br from-green-500 to-lime-500'; case 'study': return 'bg-gradient-to-br from-indigo-500 to-blue-500'; case 'sad': return 'bg-gradient-to-br from-gray-500 to-slate-500'; case 'happy': return 'bg-gradient-to-br from-yellow-500 to-orange-500'; } } // Genre-based gradients if (genres.length > 0) { const primaryGenre = genres[0].toLowerCase(); switch (primaryGenre) { case 'pop': return 'bg-gradient-to-br from-pink-500 to-purple-500'; case 'rock': return 'bg-gradient-to-br from-red-500 to-black'; case 'hip-hop': case 'rap': return 'bg-gradient-to-br from-yellow-500 to-red-500'; case 'electronic': case 'edm': return 'bg-gradient-to-br from-cyan-500 to-blue-500'; case 'jazz': return 'bg-gradient-to-br from-amber-500 to-orange-500'; case 'classical': return 'bg-gradient-to-br from-slate-500 to-gray-600'; case 'country': return 'bg-gradient-to-br from-green-600 to-yellow-600'; case 'r&b': return 'bg-gradient-to-br from-purple-600 to-pink-600'; case 'indie': return 'bg-gradient-to-br from-teal-500 to-green-500'; case 'alternative': return 'bg-gradient-to-br from-gray-600 to-purple-600'; } } // Default gradient return 'bg-gradient-to-br from-purple-500 to-pink-500'; } export const MixedPlaylistPage = () => { const { currentUser, partnerUser, mixedPlaylists, addMixedPlaylist, removeMixedPlaylist, setMixedPlaylists, theme } = useStore(); const themeClasses = getThemeClasses(theme); const [isGenerating, setIsGenerating] = useState(false); const [isCreating, setIsCreating] = useState(false); const [newPlaylist, setNewPlaylist] = useState(null); const [vibe, setVibe] = useState(''); const [genresText, setGenresText] = useState(''); const [includeKnown, setIncludeKnown] = useState(true); const [createForBoth, setCreateForBoth] = useState(false); const parsedGenres = genresText .split(',') .map(g => g.trim()) .filter(g => g.length > 0); // Load playlists from database on component mount useEffect(() => { const loadPlaylists = async () => { if (!currentUser?.jwt) return; try { console.log('Loading playlists from database...'); const response = await apiGet<{ playlists: any[] }>('/playlists/mixed', currentUser.jwt); console.log('Loaded playlists:', response.playlists); // Convert database playlists to frontend format const convertedPlaylists = response.playlists.map((dbPlaylist: any) => { const trackUris = dbPlaylist.track_uris ? JSON.parse(dbPlaylist.track_uris) : []; console.log('Processing playlist:', dbPlaylist.name, 'track_uris count:', trackUris.length, 'raw track_uris:', dbPlaylist.track_uris?.substring(0, 100) + '...'); return { id: dbPlaylist.id, name: dbPlaylist.name, description: dbPlaylist.description, tracks: [], // We don't store full track objects in DB, just URIs createdAt: new Date(dbPlaylist.created_at), createdBy: 'AI Magic ✨', spotifyId: dbPlaylist.creator_spotify_id, spotifyUrl: dbPlaylist.creator_spotify_url, partnerSpotifyId: dbPlaylist.partner_spotify_id, partnerSpotifyUrl: dbPlaylist.partner_spotify_url, vibe: dbPlaylist.vibe, genres: dbPlaylist.genres ? JSON.parse(dbPlaylist.genres) : [], trackUris: trackUris, spotifyImageUrl: dbPlaylist.creator_spotify_image_url, }; }); // Update store with loaded playlists (replace existing ones) setMixedPlaylists(convertedPlaylists); } catch (error) { console.error('Failed to load playlists:', error); // Don't show error toast for this - it's not critical } }; loadPlaylists(); }, [currentUser?.jwt]); const handleGeneratePlaylist = async () => { if (!currentUser?.topTracks || !partnerUser?.topTracks) { toast.error('Both users need to have their music data loaded'); return; } setIsGenerating(true); try { // Simulate AI processing time await new Promise(resolve => setTimeout(resolve, 2000)); const generatedTracks = generateMixedPlaylist( currentUser.topTracks, partnerUser.topTracks, "Our Perfect Mix" ); setNewPlaylist({ id: Date.now().toString(), name: "Our Perfect Mix", description: `A beautiful blend of ${currentUser.user?.display_name} and ${partnerUser.user?.display_name}'s musical tastes`, tracks: generatedTracks, createdAt: new Date(), createdBy: 'AI Magic ✨', }); toast.success('Playlist generated successfully!'); } catch (error) { console.error('Playlist generation error:', error); toast.error('Failed to generate playlist'); } finally { setIsGenerating(false); } }; const handleCreateSpotifyPlaylist = async () => { if (!newPlaylist || !currentUser?.user?.id) { toast.error('Unable to create playlist'); return; } if (!currentUser?.jwt) { toast.error('Please login again'); return; } setIsCreating(true); try { const body: any = { partnerId: partnerUser?.user?.id || currentUser.user.id, createForBoth: false, includeKnown: true, name: newPlaylist.name, description: newPlaylist.description, }; const result = await apiPost('/playlists/mixed', body, currentUser.jwt); addMixedPlaylist({ ...newPlaylist, spotifyId: result?.createdFor?.creator?.playlistId, spotifyUrl: result?.createdFor?.creator?.url, }); toast.success('Playlist created on Spotify!'); setNewPlaylist(null); } catch (e) { console.error('Spotify playlist creation error:', e); toast.error('Failed to create playlist on Spotify'); } finally { setIsCreating(false); } }; const handleCreateEnhanced = async () => { if (!currentUser?.user?.id || !partnerUser?.user?.id) { toast.error('Missing users'); return; } if (!currentUser?.jwt) { toast.error('Please login again'); return; } setIsCreating(true); try { const body: any = { partnerId: partnerUser.user.id, createForBoth, includeKnown, vibe: vibe || undefined, genres: parsedGenres.length ? parsedGenres : undefined, // Don't pass name - let server generate unique name // name: newPlaylist?.name || 'Our Enhanced Mix', description: newPlaylist?.description || 'An AI-blended mix with fresh recommendations', }; const result = await apiPost('/playlists/mixed', body, currentUser.jwt); // Show appropriate success/error messages if (createForBoth && result.createdFor.partnerError) { toast.success('Your playlist created! Partner sync failed: ' + result.createdFor.partnerError); } else if (createForBoth && result.createdFor.partner) { toast.success('Enhanced playlist created on both accounts!'); } else { toast.success('Enhanced playlist created!'); } // Create playlist object for local store const playlistData = { id: result.id, name: result.name, // Use name from server response description: body.description, tracks: [], createdAt: new Date(), createdBy: 'AI Magic ✨', spotifyId: result.createdFor.creator.playlistId, spotifyUrl: result.createdFor.creator.url, partnerSpotifyId: result.createdFor.partner?.playlistId, partnerSpotifyUrl: result.createdFor.partner?.url, vibe: body.vibe, genres: body.genres || [], trackUris: result.trackUris || [], // Use track URIs from server response spotifyImageUrl: result.spotifyImageUrl, // Use image URL from server response }; addMixedPlaylist(playlistData); setNewPlaylist(null); } catch (e) { console.error(e); toast.error('Failed to create enhanced playlist'); } finally { setIsCreating(false); } }; const handleSaveLocally = () => { if (!newPlaylist) return; addMixedPlaylist(newPlaylist); setNewPlaylist(null); toast.success('Playlist saved locally!'); }; const PlaylistCard = ({ playlist, isNew = false }: { playlist: any; isNew?: boolean }) => (
{isNew ? ( ) : playlist.spotifyImageUrl ? ( ) : null} {!isNew && (
)}

{playlist.name}

{playlist.description}

{playlist.vibe && ( {playlist.vibe} )} {playlist.genres && playlist.genres.length > 0 && playlist.genres.slice(0, 2).map((genre: string, index: number) => ( {genre} ))}

Created {isNew ? 'just now' : new Date(playlist.createdAt).toLocaleDateString()} • {playlist.tracks.length || playlist.trackUris?.length || 0} tracks

{playlist.spotifyUrl && ( )} {playlist.partnerSpotifyUrl && ( )} {!isNew && ( )}
{/* Track List */}
{playlist.tracks.length > 0 ? ( // Show full track objects if available <> {playlist.tracks.slice(0, 5).map((track: any, index: number) => ( {track.album?.name

{track.name}

{track.artists?.[0]?.name || 'Unknown Artist'}

{formatDuration(track.duration_ms)}
))} {playlist.tracks.length > 5 && (
+{playlist.tracks.length - 5} more tracks
)} ) : playlist.trackUris && playlist.trackUris.length > 0 ? ( // Show empty space when we only have URIs
{/* Empty - no text needed */}
) : ( // No tracks available
No tracks available
)}
{/* Actions */} {isNew && (
)}
); const canGeneratePlaylist = currentUser?.topTracks && partnerUser?.topTracks; return (

Mixed Playlists

AI-powered playlists that perfectly blend your musical tastes together

{/* Generate New Playlist Section */} {canGeneratePlaylist && (

Create Your Perfect Mix

Our AI analyzes both your music tastes and creates a playlist that represents your unique musical connection

{/* Mix Generator removed as requested */}
Analyzing {currentUser.topTracks.length + partnerUser.topTracks.length} tracks from both users
{/* Enhanced controls */}
setGenresText(e.target.value)} />
)} {/* New Generated Playlist */} {newPlaylist && (

✨ Your New Playlist

)} {/* Existing Playlists */} {mixedPlaylists.length > 0 && (

Your Mixed Playlists

{mixedPlaylists.map((playlist) => ( ))}
)} {/* Empty State */} {!canGeneratePlaylist && (

{!partnerUser ? 'Waiting for your partner' : 'Loading music data'}

{!partnerUser ? 'Invite your partner to connect their Spotify account to start creating mixed playlists' : 'We need to analyze both your music tastes to create the perfect mixed playlist' }

)} {mixedPlaylists.length === 0 && canGeneratePlaylist && !newPlaylist && (

No playlists yet

Create your first mixed playlist to start your musical journey together

)}
); };