diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index e02b81e..6c90903 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -5,6 +5,7 @@ import { useQuery } from "@tanstack/react-query" import { getTracks, getStats } from "@/lib/api" import type { FilterParams, Track } from "@/lib/types" import FilterPanel from "@/components/FilterPanel" +import AudioPlayer from "@/components/AudioPlayer" // Helper function to format Discogs genre labels (e.g., "Pop---Ballad" -> ["Pop", "Ballad"]) function formatGenre(genre: string): { category: string; subgenre: string } { @@ -54,6 +55,7 @@ function extractFilterOptions(tracks: Track[]) { export default function Home() { const [filters, setFilters] = useState({}) const [page, setPage] = useState(0) + const [currentTrack, setCurrentTrack] = useState(null) const limit = 50 const { data: tracksData, isLoading: isLoadingTracks } = useQuery({ @@ -87,7 +89,7 @@ export default function Home() { {/* Main Content */} -
+
{/* Stats */} {stats && (
@@ -220,14 +222,12 @@ export default function Home() { )}
- setCurrentTrack(track)} className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700" > - Play - + {currentTrack?.id === track.id ? '▶ Playing' : 'Play'} +
+ + {/* Fixed Audio Player at bottom */} + {currentTrack && ( +
+ setCurrentTrack(null)} /> +
+ )} ) } diff --git a/frontend/components/AudioPlayer.tsx b/frontend/components/AudioPlayer.tsx new file mode 100644 index 0000000..44390ff --- /dev/null +++ b/frontend/components/AudioPlayer.tsx @@ -0,0 +1,280 @@ +"use client" + +import { useState, useRef, useEffect } from "react" +import type { Track } from "@/lib/types" + +interface AudioPlayerProps { + track: Track | null + onClose?: () => void +} + +export default function AudioPlayer({ track, onClose }: AudioPlayerProps) { + const [isPlaying, setIsPlaying] = useState(false) + const [currentTime, setCurrentTime] = useState(0) + const [duration, setDuration] = useState(0) + const [volume, setVolume] = useState(1) + const [isMuted, setIsMuted] = useState(false) + const [waveformPeaks, setWaveformPeaks] = useState([]) + const [isLoadingWaveform, setIsLoadingWaveform] = useState(false) + const [showVolumeSlider, setShowVolumeSlider] = useState(false) + + const audioRef = useRef(null) + const progressRef = useRef(null) + + // Load audio and waveform when track changes + useEffect(() => { + if (!track) { + setIsPlaying(false) + setCurrentTime(0) + setWaveformPeaks([]) + return + } + + setIsPlaying(false) + setCurrentTime(0) + loadWaveform(track.id) + + if (audioRef.current) { + audioRef.current.load() + } + }, [track?.id]) + + // Update current time as audio plays + useEffect(() => { + const audio = audioRef.current + if (!audio) return + + const updateTime = () => setCurrentTime(audio.currentTime) + const updateDuration = () => setDuration(audio.duration) + const handleEnded = () => setIsPlaying(false) + + 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) + } + }, []) + + const loadWaveform = async (trackId: string) => { + setIsLoadingWaveform(true) + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/audio/waveform/${trackId}` + ) + if (response.ok) { + const data = await response.json() + setWaveformPeaks(data.peaks || []) + } + } catch (error) { + console.error("Failed to load waveform:", error) + } finally { + setIsLoadingWaveform(false) + } + } + + const togglePlay = () => { + if (!audioRef.current) return + + if (isPlaying) { + audioRef.current.pause() + } else { + audioRef.current.play() + } + setIsPlaying(!isPlaying) + } + + const handleVolumeChange = (e: React.ChangeEvent) => { + const newVolume = parseFloat(e.target.value) + setVolume(newVolume) + if (audioRef.current) { + audioRef.current.volume = newVolume + } + if (newVolume === 0) { + setIsMuted(true) + } else if (isMuted) { + setIsMuted(false) + } + } + + const toggleMute = () => { + if (!audioRef.current) return + + if (isMuted) { + audioRef.current.volume = volume + setIsMuted(false) + } else { + audioRef.current.volume = 0 + setIsMuted(true) + } + } + + const handleWaveformClick = (e: React.MouseEvent) => { + if (!audioRef.current || !progressRef.current) return + + const rect = progressRef.current.getBoundingClientRect() + const x = e.clientX - rect.left + const percentage = x / rect.width + const newTime = percentage * duration + + audioRef.current.currentTime = newTime + setCurrentTime(newTime) + } + + const formatTime = (seconds: number) => { + if (!isFinite(seconds)) return "0:00" + const mins = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + return `${mins}:${secs.toString().padStart(2, "0")}` + } + + const getVolumeIcon = () => { + if (isMuted || volume === 0) return "🔇" + if (volume < 0.5) return "🔉" + return "🔊" + } + + if (!track) { + return null + } + + const progress = duration > 0 ? (currentTime / duration) * 100 : 0 + + return ( +
+ {/* Hidden audio element */} +