Last active
May 13, 2022 17:23
-
-
Save KirdesMF/b21f5c76babece2546304052864ff44e to your computer and use it in GitHub Desktop.
React audio player
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect, useRef, useState } from 'react'; | |
import { clsx, formatTime } from '~/utils/utils'; | |
import * as Icon from './icons'; | |
import type { Song } from '~/models/song'; | |
type Props = { | |
songs: Array<Song>; | |
}; | |
export function AudioPlayer(props: Props) { | |
// references | |
const audioRef = useRef<HTMLAudioElement>(null); // audio element | |
const containerRef = useRef<HTMLUListElement>(null); // -container list element | |
const didMountRef = useRef(false); // did mount flag | |
// states | |
const [isPlaying, setIsPlaying] = useState(false); | |
const [currentTrack, setCurrentTrack] = useState(0); | |
const [currentTime, setCurrentTime] = useState(0); | |
const [duration, setDuration] = useState(0); | |
// effects | |
// fire when song data is loaded | |
function handleLoaded() { | |
setDuration(audioRef.current?.duration ?? 0); | |
isPlaying ? audioRef.current?.play() : audioRef.current?.pause(); | |
} | |
// handle onChange event of slider | |
function handleChange(evt: React.ChangeEvent<HTMLInputElement>) { | |
audioRef.current!.currentTime = evt.target.valueAsNumber; | |
} | |
// handle onTimeUpdate event of audio element | |
function handleTimeUpdate() { | |
setCurrentTime(audioRef.current?.currentTime ?? 0); | |
} | |
function setProgressCSSVar() { | |
const value = (currentTime / duration) * 100; | |
const progress = !isNaN(value) ? value : 0; | |
return progress; | |
} | |
function handleNextTrack() { | |
if (currentTrack === props.songs.length - 1) { | |
return setCurrentTrack(0); | |
} | |
setCurrentTrack((curr) => curr + 1); | |
} | |
function handlePrevTrack() { | |
if (currentTrack === 0) { | |
return setCurrentTrack(props.songs.length - 1); | |
} | |
setCurrentTrack((curr) => curr - 1); | |
} | |
function handlePlay() { | |
isPlaying ? audioRef.current?.pause() : audioRef.current?.play(); | |
setIsPlaying((prev) => !prev); | |
} | |
function handlePlayListItem(idx: number) { | |
setCurrentTrack(idx); | |
setIsPlaying(true); | |
} | |
// use to avoid scrolling behavior on first render | |
useEffect(() => { | |
if (didMountRef.current) { | |
containerRef.current?.children[currentTrack].scrollIntoView({ | |
behavior: 'smooth', | |
}); | |
} else didMountRef.current = true; | |
}, [currentTrack]); | |
return ( | |
<div className="w-[min(100%,35rem)] grid gap-y-12"> | |
<article className="grid justify-items-center gap-y-3"> | |
<audio | |
ref={audioRef} | |
src={props.songs[currentTrack].source} | |
onTimeUpdate={handleTimeUpdate} | |
onLoadedMetadata={handleLoaded} | |
onEnded={handleNextTrack} | |
></audio> | |
<h3 className="text-clamp-xl">{props.songs[currentTrack].title}</h3> | |
<div className="flex items-center justify-center gap-5"> | |
<button | |
className="w-20 h-20 color-red rounded" | |
onClick={handlePrevTrack} | |
> | |
<Icon.PrevSVG /> | |
<span className="sr-only">Previous song</span> | |
</button> | |
<button className="w-25 h-25 color-red" onClick={handlePlay}> | |
{isPlaying ? <Icon.PauseSVG /> : <Icon.PlaySVG />} | |
<span className="sr-only">{isPlaying ? 'Pause' : 'Play'}</span> | |
</button> | |
<button className="w-20 h-20 color-red" onClick={handleNextTrack}> | |
<Icon.NextSVG /> | |
<span className="sr-only">Next song</span> | |
</button> | |
</div> | |
<div className="flex gap-5 w-full"> | |
<span className="tabular-nums">{formatTime(currentTime)}</span> | |
<label className="grid w-full"> | |
<span className="sr-only">Slider range seek track</span> | |
<input | |
className="range" | |
type="range" | |
name="seek" | |
id="seek" | |
step={0.1} | |
min={0} | |
max={duration} | |
value={currentTime} | |
onChange={handleChange} | |
style={ | |
{ | |
'--progress': `${setProgressCSSVar()}%`, | |
} as React.CSSProperties | |
} | |
/> | |
</label> | |
<span className="tabular-nums">{formatTime(duration)}</span> | |
</div> | |
</article> | |
<ul | |
ref={containerRef} | |
className="relative overflow-y-scroll max-h-60 snap-y" | |
> | |
{props.songs.map((song, index) => ( | |
<li | |
key={song.id} | |
className={clsx( | |
currentTrack === index ? 'bg-green color-black' : 'color-white', | |
'flex items-center justify-between px-2 py-1 transition-colors-200 hover:bg-green-600 hover:color-black' | |
)} | |
> | |
<span>{song.title}</span> | |
<div className="flex items-center"> | |
<button | |
className="w-8 h-8 " | |
onClick={() => { | |
handlePlayListItem(index); | |
}} | |
> | |
<Icon.PlaySVG /> | |
<span className="sr-only">Play track {song.title}</span> | |
</button> | |
<button className="w-8 h-8"> | |
<Icon.LikeSVG /> | |
<span className="sr-only">Like track {song.title}</span> | |
</button> | |
<button className="w-8 h-8"> | |
<Icon.DownloadSVG /> | |
<span className="sr-only">Download track {song.title}</span> | |
</button> | |
</div> | |
</li> | |
))} | |
</ul> | |
</div> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment