306 lines
11 KiB
TypeScript
306 lines
11 KiB
TypeScript
"use client"
|
|
|
|
import { useSession } from "next-auth/react"
|
|
import { useEffect, useState } from "react"
|
|
import { motion } from "framer-motion"
|
|
import { useRouter } from "next/navigation"
|
|
import { Heart, Music, ArrowLeft, Play, Plus, ExternalLink, Sparkles } from "lucide-react"
|
|
import Link from "next/link"
|
|
import Image from "next/image"
|
|
|
|
interface Track {
|
|
id: string
|
|
name: string
|
|
artists: Array<{ name: string }>
|
|
album: {
|
|
name: string
|
|
images: Array<{ url: string }>
|
|
}
|
|
external_urls: {
|
|
spotify: string
|
|
}
|
|
}
|
|
|
|
interface Playlist {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
external_urls: {
|
|
spotify: string
|
|
}
|
|
}
|
|
|
|
export default function MixPage() {
|
|
const { data: session, status } = useSession()
|
|
const router = useRouter()
|
|
const [recommendations, setRecommendations] = useState<Track[]>([])
|
|
const [isGenerating, setIsGenerating] = useState(false)
|
|
const [playlist, setPlaylist] = useState<Playlist | null>(null)
|
|
const [isCreatingPlaylist, setIsCreatingPlaylist] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (status === "unauthenticated") {
|
|
router.push("/auth/signin")
|
|
return
|
|
}
|
|
}, [session, status, router])
|
|
|
|
const generateMix = async () => {
|
|
setIsGenerating(true)
|
|
try {
|
|
const response = await fetch("/api/spotify/recommendations", {
|
|
method: "POST"
|
|
})
|
|
const data = await response.json()
|
|
setRecommendations(data.tracks || [])
|
|
} catch (error) {
|
|
console.error("Error generating mix:", error)
|
|
} finally {
|
|
setIsGenerating(false)
|
|
}
|
|
}
|
|
|
|
const createPlaylist = async () => {
|
|
if (recommendations.length === 0) return
|
|
|
|
setIsCreatingPlaylist(true)
|
|
try {
|
|
const response = await fetch("/api/spotify/create-playlist", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
name: "Harmony Mix 💕",
|
|
description: "A beautiful mix created for you both by Harmony",
|
|
trackIds: recommendations.map(track => track.id)
|
|
})
|
|
})
|
|
const data = await response.json()
|
|
setPlaylist(data.playlist)
|
|
} catch (error) {
|
|
console.error("Error creating playlist:", error)
|
|
} finally {
|
|
setIsCreatingPlaylist(false)
|
|
}
|
|
}
|
|
|
|
if (status === "loading") {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<motion.div
|
|
animate={{ rotate: 360 }}
|
|
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
|
|
className="w-16 h-16 border-4 border-rose-300 border-t-rose-500 rounded-full"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!session) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen p-8">
|
|
{/* Header */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6 }}
|
|
className="mb-8"
|
|
>
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<Link href="/dashboard">
|
|
<motion.button
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
className="glass-card glass-card-hover p-3 rounded-full"
|
|
>
|
|
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
|
</motion.button>
|
|
</Link>
|
|
<div>
|
|
<h1 className="text-4xl font-bold gradient-text mb-2">Mix Generator</h1>
|
|
<p className="text-gray-600 text-lg">
|
|
Create the perfect playlist for both of you
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Generate Button */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.2 }}
|
|
className="text-center mb-12"
|
|
>
|
|
<motion.button
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
onClick={generateMix}
|
|
disabled={isGenerating}
|
|
className="bg-gradient-to-r from-rose-400 to-pink-500 text-white px-12 py-6 rounded-full text-2xl font-semibold shadow-2xl hover:shadow-3xl transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-4 mx-auto"
|
|
>
|
|
{isGenerating ? (
|
|
<>
|
|
<motion.div
|
|
animate={{ rotate: 360 }}
|
|
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
|
className="w-8 h-8 border-2 border-white border-t-transparent rounded-full"
|
|
/>
|
|
Creating Your Mix...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Music className="w-8 h-8" />
|
|
Make us a Mix 💽
|
|
<Sparkles className="w-8 h-8" />
|
|
</>
|
|
)}
|
|
</motion.button>
|
|
</motion.div>
|
|
|
|
{/* Recommendations */}
|
|
{recommendations.length > 0 && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.4 }}
|
|
className="mb-8"
|
|
>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-semibold text-gray-800">
|
|
Your Perfect Mix ({recommendations.length} songs)
|
|
</h2>
|
|
{!playlist && (
|
|
<motion.button
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
onClick={createPlaylist}
|
|
disabled={isCreatingPlaylist}
|
|
className="bg-gradient-to-r from-green-500 to-green-600 text-white px-6 py-3 rounded-full font-semibold shadow-lg hover:shadow-xl transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
{isCreatingPlaylist ? (
|
|
<>
|
|
<motion.div
|
|
animate={{ rotate: 360 }}
|
|
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
|
className="w-4 h-4 border-2 border-white border-t-transparent rounded-full"
|
|
/>
|
|
Creating...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Plus className="w-4 h-4" />
|
|
Create Playlist
|
|
</>
|
|
)}
|
|
</motion.button>
|
|
)}
|
|
</div>
|
|
|
|
{playlist && (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
className="glass-card glass-card-hover p-6 mb-6 text-center"
|
|
>
|
|
<div className="w-16 h-16 bg-gradient-to-br from-green-400 to-green-500 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<Play className="w-8 h-8 text-white" />
|
|
</div>
|
|
<h3 className="text-xl font-semibold text-gray-800 mb-2">Playlist Created!</h3>
|
|
<p className="text-gray-600 mb-4">Your mix is ready to enjoy together</p>
|
|
<a
|
|
href={playlist.external_urls.spotify}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-2 bg-gradient-to-r from-green-500 to-green-600 text-white px-6 py-3 rounded-full font-semibold shadow-lg hover:shadow-xl transition-all duration-300"
|
|
>
|
|
Open in Spotify
|
|
<ExternalLink className="w-4 h-4" />
|
|
</a>
|
|
</motion.div>
|
|
)}
|
|
|
|
<div className="grid gap-4">
|
|
{recommendations.map((track, index) => (
|
|
<motion.div
|
|
key={track.id}
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.6, delay: index * 0.05 }}
|
|
className="glass-card glass-card-hover p-6"
|
|
>
|
|
<div className="flex items-center gap-6">
|
|
{/* Album Cover */}
|
|
<div className="relative">
|
|
{track.album.images[0] ? (
|
|
<Image
|
|
src={track.album.images[0].url}
|
|
alt={track.album.name}
|
|
width={80}
|
|
height={80}
|
|
className="rounded-xl shadow-lg"
|
|
/>
|
|
) : (
|
|
<div className="w-20 h-20 bg-gradient-to-br from-gray-300 to-gray-400 rounded-xl flex items-center justify-center">
|
|
<Music className="w-8 h-8 text-white" />
|
|
</div>
|
|
)}
|
|
<motion.div
|
|
animate={{ scale: [1, 1.1, 1] }}
|
|
transition={{ duration: 2, repeat: Infinity }}
|
|
className="absolute -top-2 -right-2 w-6 h-6 bg-gradient-to-br from-rose-400 to-pink-500 rounded-full flex items-center justify-center shadow-lg"
|
|
>
|
|
<Heart className="w-3 h-3 text-white" />
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Track Info */}
|
|
<div className="flex-1">
|
|
<h3 className="text-xl font-semibold text-gray-800 mb-1">
|
|
{track.name}
|
|
</h3>
|
|
<p className="text-gray-600 text-lg mb-1">
|
|
{track.artists.map(artist => artist.name).join(", ")}
|
|
</p>
|
|
<p className="text-gray-500">{track.album.name}</p>
|
|
</div>
|
|
|
|
{/* Spotify Link */}
|
|
<a
|
|
href={track.external_urls.spotify}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="glass-card glass-card-hover p-3 rounded-full"
|
|
>
|
|
<ExternalLink className="w-5 h-5 text-gray-600" />
|
|
</a>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{recommendations.length === 0 && !isGenerating && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
className="text-center py-16"
|
|
>
|
|
<div className="w-24 h-24 bg-gradient-to-br from-rose-300 to-pink-400 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
<Music className="w-12 h-12 text-white" />
|
|
</div>
|
|
<h3 className="text-2xl font-semibold text-gray-800 mb-4">Ready to create your mix?</h3>
|
|
<p className="text-gray-600 max-w-md mx-auto">
|
|
Click the button above to generate a personalized playlist based on both of your musical tastes.
|
|
</p>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|