690 lines
26 KiB
TypeScript
690 lines
26 KiB
TypeScript
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<string | null>(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 (
|
|
<div className="w-full h-full bg-gradient-to-br from-gray-500 to-gray-600 flex items-center justify-center">
|
|
<Music className="w-8 h-8 text-white animate-pulse" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !src) {
|
|
return (
|
|
<div className={`w-full h-full flex items-center justify-center ${fallbackGradient}`}>
|
|
<Music className="w-8 h-8 text-white" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<img
|
|
src={src}
|
|
alt={alt}
|
|
className="w-full h-full object-cover"
|
|
onError={() => 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<any>(null);
|
|
const [vibe, setVibe] = useState<string>('');
|
|
const [genresText, setGenresText] = useState<string>('');
|
|
const [includeKnown, setIncludeKnown] = useState<boolean>(true);
|
|
const [createForBoth, setCreateForBoth] = useState<boolean>(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 }) => (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
whileHover={{ scale: 1.02 }}
|
|
className={`glass rounded-2xl p-6 ${isNew ? 'border-spotify-green/50' : 'border-white/10'}`}
|
|
>
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex items-center space-x-4">
|
|
<div className={`w-16 h-16 rounded-xl flex items-center justify-center overflow-hidden flex-shrink-0 ${
|
|
isNew ? 'bg-gradient-to-br from-spotify-green to-green-600' : 'bg-gradient-to-br from-purple-500 to-pink-500'
|
|
}`}>
|
|
{isNew ? (
|
|
<Sparkles className="w-8 h-8 text-white" />
|
|
) : playlist.spotifyImageUrl ? (
|
|
<PlaylistImage
|
|
imageUrl={playlist.spotifyImageUrl}
|
|
alt={playlist.name}
|
|
fallbackGradient={getPlaylistGradient(playlist)}
|
|
/>
|
|
) : null}
|
|
{!isNew && (
|
|
<div
|
|
className={`w-full h-full flex items-center justify-center ${
|
|
playlist.spotifyImageUrl
|
|
? 'bg-gradient-to-br from-purple-500 to-pink-500'
|
|
: getPlaylistGradient(playlist)
|
|
}`}
|
|
style={{ display: playlist.spotifyImageUrl ? 'none' : 'flex' }}
|
|
>
|
|
<Music className="w-8 h-8 text-white" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xl font-semibold text-white">{playlist.name}</h3>
|
|
<p className="text-white/70 text-sm">{playlist.description}</p>
|
|
<div className="flex flex-wrap gap-1 mt-1 mb-1">
|
|
{playlist.vibe && (
|
|
<span className="px-2 py-0.5 bg-blue-500/20 text-blue-300 text-xs rounded-full">
|
|
{playlist.vibe}
|
|
</span>
|
|
)}
|
|
{playlist.genres && playlist.genres.length > 0 && playlist.genres.slice(0, 2).map((genre: string, index: number) => (
|
|
<span key={index} className="px-2 py-0.5 bg-green-500/20 text-green-300 text-xs rounded-full">
|
|
{genre}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<p className="text-white/50 text-xs">
|
|
Created {isNew ? 'just now' : new Date(playlist.createdAt).toLocaleDateString()} • {playlist.tracks.length || playlist.trackUris?.length || 0} tracks
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
{playlist.spotifyUrl && (
|
|
<a
|
|
href={playlist.spotifyUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
|
|
title="Open in Spotify"
|
|
>
|
|
<ExternalLink className="w-4 h-4 text-white" />
|
|
</a>
|
|
)}
|
|
{playlist.partnerSpotifyUrl && (
|
|
<a
|
|
href={playlist.partnerSpotifyUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="w-8 h-8 rounded-full bg-pink-500/20 hover:bg-pink-500/30 flex items-center justify-center transition-colors"
|
|
title="Partner's playlist in Spotify"
|
|
>
|
|
<Heart className="w-4 h-4 text-pink-400" />
|
|
</a>
|
|
)}
|
|
|
|
{!isNew && (
|
|
<button
|
|
onClick={async () => {
|
|
try {
|
|
if (currentUser?.jwt) {
|
|
await apiDelete(`/playlists/mixed/${playlist.id}`, currentUser.jwt);
|
|
}
|
|
removeMixedPlaylist(playlist.id);
|
|
toast.success('Playlist deleted successfully');
|
|
} catch (error) {
|
|
console.error('Failed to delete playlist:', error);
|
|
toast.error('Failed to delete playlist');
|
|
}
|
|
}}
|
|
className="w-8 h-8 rounded-full bg-red-500/20 hover:bg-red-500/30 flex items-center justify-center transition-colors"
|
|
>
|
|
<Trash2 className="w-4 h-4 text-red-400" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Track List */}
|
|
<div className="space-y-3 max-h-64 overflow-y-auto">
|
|
{playlist.tracks.length > 0 ? (
|
|
// Show full track objects if available
|
|
<>
|
|
{playlist.tracks.slice(0, 5).map((track: any, index: number) => (
|
|
<motion.div
|
|
key={track.id}
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: index * 0.1 }}
|
|
className="flex items-center space-x-3 p-2 rounded-lg hover:bg-white/5 transition-colors"
|
|
>
|
|
<img
|
|
src={track.album?.images?.[0]?.url || '/placeholder-album.png'}
|
|
alt={track.album?.name || 'Unknown Album'}
|
|
className="w-10 h-10 rounded-lg object-cover"
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className="text-white font-medium truncate">{track.name}</h4>
|
|
<p className="text-white/70 text-sm truncate">{track.artists?.[0]?.name || 'Unknown Artist'}</p>
|
|
</div>
|
|
<div className="text-white/50 text-sm">
|
|
{formatDuration(track.duration_ms)}
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
{playlist.tracks.length > 5 && (
|
|
<div className="text-center text-white/50 text-sm py-2">
|
|
+{playlist.tracks.length - 5} more tracks
|
|
</div>
|
|
)}
|
|
</>
|
|
) : playlist.trackUris && playlist.trackUris.length > 0 ? (
|
|
// Show empty space when we only have URIs
|
|
<div className="text-center py-4">
|
|
{/* Empty - no text needed */}
|
|
</div>
|
|
) : (
|
|
// No tracks available
|
|
<div className="text-center text-white/50 text-sm py-4">
|
|
No tracks available
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
{isNew && (
|
|
<div className="flex items-center space-x-3 mt-6 pt-4 border-t border-white/10">
|
|
<button
|
|
onClick={handleCreateSpotifyPlaylist}
|
|
disabled={isCreating || createForBoth}
|
|
className="flex-1 bg-spotify-green hover:bg-spotify-green/90 text-white font-semibold py-3 px-4 rounded-lg transition-colors flex items-center justify-center space-x-2 disabled:opacity-50"
|
|
>
|
|
{isCreating ? (
|
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
) : (
|
|
<>
|
|
<Plus className="w-4 h-4" />
|
|
<span>{createForBoth ? 'Disabled (using Create for both)' : 'Create on Spotify'}</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleSaveLocally}
|
|
className="px-4 py-3 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors"
|
|
>
|
|
Save Locally
|
|
</button>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
);
|
|
|
|
const canGeneratePlaylist = currentUser?.topTracks && partnerUser?.topTracks;
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="mb-8"
|
|
>
|
|
<h1 className="text-4xl font-bold text-white mb-2 flex items-center space-x-3">
|
|
<Sparkles
|
|
className="w-10 h-10"
|
|
style={{ color: themeClasses.cssVars?.primary || '#1db954' }}
|
|
/>
|
|
<span>Mixed Playlists</span>
|
|
</h1>
|
|
<p className="text-white/70 text-lg">
|
|
AI-powered playlists that perfectly blend your musical tastes together
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* Generate New Playlist Section */}
|
|
{canGeneratePlaylist && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.1 }}
|
|
className="mb-8"
|
|
>
|
|
<div
|
|
className="glass rounded-2xl p-8 text-center"
|
|
style={{ borderColor: `${themeClasses.cssVars?.primary || '#1db954'}30` }}
|
|
>
|
|
<div
|
|
className="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center"
|
|
style={{
|
|
background: `linear-gradient(135deg, ${themeClasses.cssVars?.primary || '#1db954'} 0%, ${themeClasses.cssVars?.secondary || '#1ed760'} 100%)`,
|
|
}}
|
|
>
|
|
<Wand2 className="w-8 h-8 text-white" />
|
|
</div>
|
|
|
|
<h2 className="text-2xl font-bold text-white mb-2">Create Your Perfect Mix</h2>
|
|
<p className="text-white/70 mb-6 max-w-2xl mx-auto">
|
|
Our AI analyzes both your music tastes and creates a playlist that represents your unique musical connection
|
|
</p>
|
|
|
|
{/* Mix Generator removed as requested */}
|
|
|
|
<div className="mt-4 text-white/50 text-sm">
|
|
Analyzing {currentUser.topTracks.length + partnerUser.topTracks.length} tracks from both users
|
|
</div>
|
|
|
|
{/* Enhanced controls */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left mt-8">
|
|
<div>
|
|
<label className="block text-white/80 text-sm mb-1">Vibe</label>
|
|
<select
|
|
className="w-full bg-white/10 text-white rounded-lg p-2 appearance-none focus:outline-none focus:ring-2 focus:ring-spotify-green"
|
|
value={vibe}
|
|
onChange={(e) => setVibe(e.target.value)}
|
|
style={{
|
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
color: 'rgba(255, 255, 255, 0.9)'
|
|
}}
|
|
>
|
|
<option value="" style={{ backgroundColor: 'rgba(30, 30, 30, 0.95)', color: 'rgba(255, 255, 255, 0.9)' }}>Auto</option>
|
|
<option value="energetic" style={{ backgroundColor: 'rgba(30, 30, 30, 0.95)', color: 'rgba(255, 255, 255, 0.9)' }}>Energetic</option>
|
|
<option value="chill" style={{ backgroundColor: 'rgba(30, 30, 30, 0.95)', color: 'rgba(255, 255, 255, 0.9)' }}>Chill</option>
|
|
<option value="happy" style={{ backgroundColor: 'rgba(30, 30, 30, 0.95)', color: 'rgba(255, 255, 255, 0.9)' }}>Happy</option>
|
|
<option value="sad" style={{ backgroundColor: 'rgba(30, 30, 30, 0.95)', color: 'rgba(255, 255, 255, 0.9)' }}>Sad</option>
|
|
<option value="party" style={{ backgroundColor: 'rgba(30, 30, 30, 0.95)', color: 'rgba(255, 255, 255, 0.9)' }}>Party</option>
|
|
<option value="focus" style={{ backgroundColor: 'rgba(30, 30, 30, 0.95)', color: 'rgba(255, 255, 255, 0.9)' }}>Focus</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-white/80 text-sm mb-1">Genres (comma-separated)</label>
|
|
<input
|
|
className="w-full bg-white/10 text-white rounded-lg p-2 placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-spotify-green"
|
|
placeholder="pop, rock, edm"
|
|
value={genresText}
|
|
onChange={(e) => setGenresText(e.target.value)}
|
|
/>
|
|
</div>
|
|
<label className="flex items-center space-x-2 text-white/80">
|
|
<input type="checkbox" checked={includeKnown} onChange={(e) => setIncludeKnown(e.target.checked)} />
|
|
<span>Include songs we already know</span>
|
|
</label>
|
|
<label className="flex items-center space-x-2 text-white/80">
|
|
<input type="checkbox" checked={createForBoth} onChange={(e) => setCreateForBoth(e.target.checked)} />
|
|
<span>Create playlist on both accounts</span>
|
|
{createForBoth && (
|
|
<span className="text-xs text-pink-400 ml-2">✨ Synced</span>
|
|
)}
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-center gap-3 mt-6">
|
|
<button
|
|
onClick={handleCreateEnhanced}
|
|
disabled={isCreating}
|
|
className="bg-white/10 hover:bg-white/20 text-white font-semibold py-3 px-4 rounded-lg transition-colors disabled:opacity-50"
|
|
>
|
|
Create Enhanced Playlist
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* New Generated Playlist */}
|
|
{newPlaylist && (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
className="mb-8"
|
|
>
|
|
<h2 className="text-2xl font-bold text-white mb-4">✨ Your New Playlist</h2>
|
|
<PlaylistCard playlist={newPlaylist} isNew={true} />
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Existing Playlists */}
|
|
{mixedPlaylists.length > 0 && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.3 }}
|
|
>
|
|
<h2 className="text-2xl font-bold text-white mb-4">Your Mixed Playlists</h2>
|
|
<div className="grid gap-6">
|
|
{mixedPlaylists.map((playlist) => (
|
|
<PlaylistCard key={playlist.id} playlist={playlist} />
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Empty State */}
|
|
{!canGeneratePlaylist && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="glass rounded-2xl p-12 text-center"
|
|
>
|
|
<Heart className="w-16 h-16 mx-auto mb-4 text-pink-400" />
|
|
<h3 className="text-xl font-semibold text-white mb-2">
|
|
{!partnerUser ? 'Waiting for your partner' : 'Loading music data'}
|
|
</h3>
|
|
<p className="text-white/70">
|
|
{!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'
|
|
}
|
|
</p>
|
|
</motion.div>
|
|
)}
|
|
|
|
{mixedPlaylists.length === 0 && canGeneratePlaylist && !newPlaylist && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="glass rounded-2xl p-12 text-center"
|
|
>
|
|
<Music className="w-16 h-16 mx-auto mb-4 text-white/30" />
|
|
<h3 className="text-xl font-semibold text-white mb-2">No playlists yet</h3>
|
|
<p className="text-white/70">
|
|
Create your first mixed playlist to start your musical journey together
|
|
</p>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|