AudioForge / frontend /src /components /audio-player.tsx
AudioForge Deploy
chore: pre-deployment polish & fixes
5bf2d26
"use client";
import { useState, useRef, useEffect } from "react";
import { Play, Pause, Volume2, VolumeX } from "lucide-react";
import { Slider } from "@/components/ui/slider";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface AudioPlayerProps {
src: string;
className?: string;
autoplay?: boolean;
onPlayStateChange?: (isPlaying: boolean) => void;
}
export function AudioPlayer({ src, className, autoplay = false, onPlayStateChange }: AudioPlayerProps) {
const [isPlaying, setIsPlaying] = useState(autoplay);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [volume, setVolume] = useState(1);
const [isMuted, setIsMuted] = useState(false);
const audioRef = useRef<HTMLAudioElement>(null);
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
if (autoplay) {
audio.play().catch(() => {
/* Autoplay blocked by browser */
});
}
const setAudioData = () => {
setDuration(audio.duration);
setVolume(audio.volume);
};
const setAudioTime = () => {
setCurrentTime(audio.currentTime);
};
const handleEnded = () => {
setIsPlaying(false);
onPlayStateChange?.(false);
};
// Add event listeners
audio.addEventListener("loadeddata", setAudioData);
audio.addEventListener("timeupdate", setAudioTime);
audio.addEventListener("ended", handleEnded);
return () => {
audio.removeEventListener("loadeddata", setAudioData);
audio.removeEventListener("timeupdate", setAudioTime);
audio.removeEventListener("ended", handleEnded);
};
}, [src, autoplay, onPlayStateChange]);
const togglePlay = () => {
const audio = audioRef.current;
if (!audio) return;
if (isPlaying) {
audio.pause();
onPlayStateChange?.(false);
} else {
audio.play();
onPlayStateChange?.(true);
}
setIsPlaying(!isPlaying);
};
const toggleMute = () => {
const audio = audioRef.current;
if (!audio) return;
audio.muted = !isMuted;
setIsMuted(!isMuted);
};
const handleVolumeChange = (value: number[]) => {
const audio = audioRef.current;
if (!audio) return;
const newVolume = value[0];
audio.volume = newVolume;
setVolume(newVolume);
if (newVolume > 0 && isMuted) {
setIsMuted(false);
audio.muted = false;
}
};
const handleSeek = (value: number[]) => {
const audio = audioRef.current;
if (!audio) return;
audio.currentTime = value[0];
setCurrentTime(value[0]);
};
const formatTime = (time: number) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
};
return (
<div className={cn("flex flex-col gap-2 w-full bg-background/50 rounded-lg border p-3", className)}>
<audio ref={audioRef} src={src} preload="metadata" />
<div className="flex items-center gap-2">
{!isPlaying ? (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 bg-primary/10 hover:bg-primary/20 text-primary"
onClick={togglePlay}
aria-label="Play"
>
<Play className="h-4 w-4 ml-0.5 fill-current" />
</Button>
) : (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 bg-secondary hover:bg-secondary/80"
onClick={togglePlay}
aria-label="Pause"
>
<Pause className="h-4 w-4 fill-current" />
</Button>
)}
<div className="flex-1 flex flex-col gap-1 ml-1">
<Slider
value={[currentTime]}
max={duration || 100} // Default to 100 to avoid 0 width slider initially
step={0.1}
onValueChange={handleSeek}
className="w-full cursor-pointer"
aria-label="Seek"
/>
<div className="flex justify-between text-[10px] text-muted-foreground px-0.5">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
<div className="flex items-center gap-2 group relative">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
onClick={toggleMute}
aria-label={isMuted ? "Unmute" : "Mute"}
>
{isMuted || volume === 0 ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
<div className="w-0 overflow-hidden group-hover:w-20 transition-all duration-300">
<Slider
value={[isMuted ? 0 : volume]}
max={1}
step={0.01}
onValueChange={handleVolumeChange}
className="w-20 pr-2"
aria-label="Volume"
/>
</div>
</div>
</div>
</div>
);
}