podzahr/components/MusicPlayer.tsx
nfel 9a7e627329
main: second iter
Signed-off-by: nfel <nfilsaraee@gmail.com>
2025-12-27 22:41:36 +03:30

339 lines
13 KiB
TypeScript

'use client';
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { FaPlay, FaPause, FaStepBackward, FaStepForward, FaTimes, FaChevronDown, FaChevronUp } from 'react-icons/fa';
import { Album, Song } from '@/lib/types';
interface MusicPlayerProps {
album: Album | null;
isPurchased: boolean;
onClose: () => void;
onPurchase: (album: Album) => void;
}
export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }: MusicPlayerProps) {
const [currentSongIndex, setCurrentSongIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [showTrackList, setShowTrackList] = useState(false);
const audioRef = useRef<HTMLAudioElement>(null);
const currentSong = album?.songs[currentSongIndex];
useEffect(() => {
if (album) {
setCurrentSongIndex(0);
setIsPlaying(false);
setCurrentTime(0);
}
}, [album]);
useEffect(() => {
const audio = audioRef.current;
console.log(audioRef.current)
if (!audio) return;
const updateTime = () => setCurrentTime(audio.currentTime);
const updateDuration = () => setDuration(audio.duration);
const handleEnded = () => {
if (!isPurchased) {
// Preview ended
setIsPlaying(false);
setCurrentTime(0);
} else {
// Move to next song
handleNext();
}
};
audio.addEventListener('timeupdate', updateTime);
audio.addEventListener('loadedmetadata', updateDuration);
audio.addEventListener('ended', handleEnded);
return () => {
audio.removeEventListener('timeupdate', updateTime);
audio.removeEventListener('loadedmetadata', updateDuration);
audio.removeEventListener('ended', handleEnded);
};
}, [isPurchased, currentSongIndex, album]);
const togglePlay = () => {
const audio = audioRef.current;
if (!audio) return;
if (isPlaying) {
audio.pause();
} else {
audio.play();
}
setIsPlaying(!isPlaying);
};
const handleNext = () => {
if (!album) return;
const nextIndex = (currentSongIndex + 1) % album.songs.length;
setCurrentSongIndex(nextIndex);
setIsPlaying(false);
setCurrentTime(0);
};
const handlePrevious = () => {
if (!album) return;
const prevIndex = currentSongIndex === 0 ? album.songs.length - 1 : currentSongIndex - 1;
setCurrentSongIndex(prevIndex);
setIsPlaying(false);
setCurrentTime(0);
};
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
const audio = audioRef.current;
if (!audio || !isPurchased) return; // Only allow seeking for purchased albums
const time = parseFloat(e.target.value);
audio.currentTime = time;
setCurrentTime(time);
};
const formatTime = (time: number) => {
if (isNaN(time)) return '0:00';
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
if (!album) return null;
return (
<AnimatePresence>
{album && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/90 backdrop-blur-xl z-50"
/>
{/* Player Modal */}
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 50 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 50 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
className="fixed inset-0 z-50 flex items-center justify-center p-6 md:p-12 pointer-events-none"
>
<div className="w-full max-w-sm pointer-events-auto relative">
{/* Close Button - Top Right */}
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={onClose}
className="absolute -top-2 -right-2 z-10 p-3 bg-white/90 hover:bg-white rounded-full transition-colors shadow-xl"
aria-label="Close player"
>
<FaTimes className="text-xl text-primary-900" />
</motion.button>
{/* Album Artwork */}
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.1 }}
className="relative aspect-square rounded-2xl overflow-hidden mb-6 shadow-2xl"
>
<div className="absolute inset-0 bg-gradient-to-br from-primary-500 via-primary-700 to-primary-900"></div>
<div className="absolute inset-0 bg-gradient-to-br from-accent-cyan/30 to-accent-orange/30"></div>
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-4xl md:text-5xl font-bold text-white/30 text-center px-6">
{album.title}
</div>
</div>
{/* Preview Badge */}
{!isPurchased && (
<div className="absolute top-3 right-3 bg-accent-orange/90 backdrop-blur-sm text-white px-3 py-1 rounded-full text-xs font-semibold shadow-lg">
Preview Mode
</div>
)}
</motion.div>
{/* Song Info */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-center mb-6"
>
<h2 className="text-xl md:text-2xl font-bold text-white mb-1">
{currentSong?.title}
</h2>
<p className="text-base text-gray-400">
{album.title}
</p>
<p className="text-xs text-gray-500 mt-1">
Track {currentSongIndex + 1} of {album.songs.length}
</p>
</motion.div>
{/* Progress Bar */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mb-5"
>
<div className="relative">
<input
type="range"
min="0"
max={duration || 0}
value={currentTime}
onChange={handleSeek}
disabled={!isPurchased}
className="w-full h-1 bg-gray-700/50 rounded-full appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, #00d9ff ${(currentTime / (duration || 1)) * 100}%, rgba(255,255,255,0.1) ${(currentTime / (duration || 1)) * 100}%)`,
}}
/>
</div>
<div className="flex justify-between text-xs text-gray-400 mt-1.5 font-mono">
<span>{formatTime(currentTime)}</span>
<span>{isPurchased ? formatTime(duration) : '0:30'}</span>
</div>
</motion.div>
{/* Controls */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="flex items-center justify-center gap-6 mb-6"
>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={handlePrevious}
className="p-3 hover:bg-white/10 rounded-full transition-colors"
aria-label="Previous track"
>
<FaStepBackward className="text-xl text-white" />
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={togglePlay}
className="p-5 bg-white hover:bg-gray-100 rounded-full transition-all shadow-2xl"
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? (
<FaPause className="text-2xl text-primary-900" />
) : (
<FaPlay className="text-2xl text-primary-900 ml-0.5" />
)}
</motion.button>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={handleNext}
className="p-3 hover:bg-white/10 rounded-full transition-colors"
aria-label="Next track"
>
<FaStepForward className="text-xl text-white" />
</motion.button>
</motion.div>
{/* Purchase Button */}
{!isPurchased && (
<motion.button
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => onPurchase(album)}
className="w-full mb-3 py-3 bg-gradient-to-r from-accent-orange to-accent-orange/80 hover:from-accent-orange/90 hover:to-accent-orange/70 rounded-xl font-semibold text-white text-base transition-all shadow-xl glow-orange"
>
Purchase Full Album - ${album.price}
</motion.button>
)}
{/* Track List Toggle */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
>
<button
onClick={() => setShowTrackList(!showTrackList)}
className="w-full py-2.5 bg-white/5 hover:bg-white/10 rounded-lg transition-colors flex items-center justify-center gap-2 text-gray-300 text-sm"
>
{showTrackList ? <FaChevronUp className="text-xs" /> : <FaChevronDown className="text-xs" />}
<span className="font-medium">
{showTrackList ? 'Hide' : 'Show'} Track List
</span>
</button>
<AnimatePresence>
{showTrackList && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="mt-3 max-h-52 overflow-y-auto rounded-lg bg-white/5 p-2">
<div className="space-y-0.5">
{album.songs.map((song, index) => (
<button
key={song.id}
onClick={() => {
setCurrentSongIndex(index);
setIsPlaying(false);
setCurrentTime(0);
}}
className={`w-full text-left px-3 py-2 rounded-md transition-all ${
index === currentSongIndex
? 'bg-accent-cyan/20 text-accent-cyan'
: 'hover:bg-white/5 text-gray-300'
}`}
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 font-mono w-5">
{String(index + 1).padStart(2, '0')}
</span>
<span className="text-sm font-medium">{song.title}</span>
</div>
<span className="text-xs text-gray-500 font-mono">{song.duration}</span>
</div>
</button>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
{/* Hidden Audio Element */}
<audio
ref={audioRef}
src={isPurchased ? currentSong?.fullUrl : currentSong?.previewUrl}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}