Last active
September 14, 2024 18:21
-
-
Save EvanMarie/17f1784a861db984a3990a45ae5cd005 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 { useRef, useState } from "react"; | |
import { motion } from "framer-motion"; | |
import FlexFull from "~/components/buildingBlocks/flexFull"; | |
import Text from "~/components/buildingBlocks/text"; | |
import VStackFull from "~/components/buildingBlocks/vStackFull"; | |
import { useAudioPlayer } from "~/hooks/useAudioPlayer"; | |
import HStack from "~/components/buildingBlocks/hStack"; | |
import Icon from "~/components/buildingBlocks/icon"; | |
import { FaPlay, FaStop } from "react-icons/fa"; | |
import Tooltip from "~/components/buildingBlocks/tooltip"; | |
import { BsPlayFill, BsStopFill } from "react-icons/bs"; | |
import { HiVolumeUp } from "react-icons/hi"; | |
import Box from "~/components/buildingBlocks/box"; | |
interface AudioPlayerProps { | |
buttonOnly?: boolean; | |
darkMode?: boolean; | |
audioUrl?: string; | |
audioName?: string; | |
baseAudioUrl?: string; | |
border?: string; | |
shadow?: string; | |
bg?: string; | |
title?: string; | |
showTitle?: boolean; | |
containerPadding?: string; | |
} | |
export default function AudioPlayer({ | |
buttonOnly, | |
darkMode = true, | |
audioUrl, | |
audioName, | |
bg = darkMode | |
? "bg-slate-900 bg-gradient-to-br from-indigo-900/30 via-indigo-900/20 to-indigo-900/30" | |
: "bg-col-500 bg-gradient-to-br from-indigo-300/30 via-indigo-300/20 to-indigo-300/30", | |
border = darkMode ? "border-900-lg" : "border-200-md", | |
shadow = darkMode ? "insetShadow6xl" : "insetGlow6xl", | |
baseAudioUrl = "https://database-base-url.com, | |
showTitle = false, | |
containerPadding = "px-[3vh] pt-[0.3vh]", | |
}: AudioPlayerProps) { | |
const audioRef = useRef<HTMLAudioElement>(null); | |
const { | |
isPlaying, | |
currentTime, | |
duration, | |
togglePlayPause, | |
handleTimeUpdate, | |
handleLoadedMetadata, | |
handleSeek, | |
} = useAudioPlayer(audioRef); | |
// Use a local state to manually reset the currentTime to 0 on audio end | |
const [sliderValue, setSliderValue] = useState(0); | |
const url = audioUrl ? audioUrl : `${baseAudioUrl}/${audioName}`; | |
// Handle resetting the audio and range slider on audio end | |
const handleAudioEnd = () => { | |
setSliderValue(0); // Reset slider to 0 | |
}; | |
// Sync slider with audio time | |
const handleTimeUpdateWithSync = () => { | |
if (audioRef.current) { | |
setSliderValue(audioRef.current.currentTime); // Sync slider value with current time | |
} | |
handleTimeUpdate(); // Call the original time update logic | |
}; | |
const iconStyles = `${ | |
darkMode | |
? "text-col-500" | |
: "text-col-900 border-900-sm bg-col-260 hover:bg-col-200 transition-300 shadowNarrowTight" | |
} p-[0.4vh] rounded-full text-[2.5vh] h-full flex justify-center items-center hover:metallicEdgesXs transition-300 `; | |
const darkModeSlideStyle = ` | |
bg-purple-300 | |
[&::-moz-range-thumb]:border-900-sm | |
[&::-moz-range-thumb]:hover:boxGlowXs | |
[&::-moz-range-thumb]:hover:bg-sky-400 | |
[&::-moz-range-thumb]:bg-col-500 | |
[&::-webkit-slider-thumb]:border-900-sm | |
[&::-webkit-slider-thumb]:hover:boxGlowXs | |
[&::-webkit-slider-thumb]:hover:bg-sky-400 | |
[&::-webkit-slider-thumb]:bg-col-500 | |
transition-300 | |
`; | |
const lightModeSlideStyle = ` | |
bg-col-800 | |
[&::-moz-range-thumb]:border-200-md | |
[&::-moz-range-thumb]:hover:boxGlowXs | |
[&::-moz-range-thumb]:hover:bg-col-600 | |
[&::-moz-range-thumb]:bg-col-700 | |
[&::-webkit-slider-thumb]:border-200-md | |
[&::-webkit-slider-thumb]:hover:metallicEdgesSm | |
[&::-webkit-slider-thumb]:hover:bg-col-600 | |
[&::-webkit-slider-thumb]:bg-col-700 | |
transition-300 | |
`; | |
return ( | |
<> | |
{buttonOnly ? ( | |
<> | |
<audio | |
ref={audioRef} | |
src={url} | |
onTimeUpdate={handleTimeUpdateWithSync} // Sync slider with audio time | |
onLoadedMetadata={handleLoadedMetadata} | |
onEnded={handleAudioEnd} // Reset slider when audio ends | |
className="w-full" | |
/> | |
<Tooltip placement="topRight" label="listen"> | |
<motion.button | |
onClick={togglePlayPause} | |
className={`${iconStyles}}`} | |
> | |
{isPlaying ? ( | |
<BsStopFill style={{ fontSize: "inherit" }} /> | |
) : ( | |
<BsPlayFill style={{ fontSize: "inherit" }} /> | |
)} | |
</motion.button> | |
</Tooltip> | |
</> | |
) : ( | |
<FlexFull | |
className={`relative h-fit rounded-[6vh] ${bg} ${border} ${shadow}`} | |
> | |
<Box | |
className={`absolute -top-[1vh] left-[1vh] bg-col-800 border-200-sm metallicEdgesSm p-[0.4vh] rounded-full`} | |
> | |
<Icon | |
icon={HiVolumeUp} | |
iconClassName={`text-col-200 text-[2.4vh]`} | |
/> | |
</Box> | |
<VStackFull | |
className={`${containerPadding} overflow-hidden`} | |
gap="gap-[0px]" | |
> | |
<audio | |
ref={audioRef} | |
src={url} | |
onTimeUpdate={handleTimeUpdateWithSync} // Sync slider with audio time | |
onLoadedMetadata={handleLoadedMetadata} | |
onEnded={handleAudioEnd} // Reset slider when audio ends | |
className="w-full" | |
/> | |
{title && showTitle && ( | |
<FlexFull className=" justify-center items-center pb-[0.5vh]"> | |
<Text | |
noOfLines={1} | |
className={`text-[2vh] ${ | |
darkMode | |
? "text-col-500 textShadow" | |
: "text-col-900 font-semibold lightTextShadow" | |
}`} | |
> | |
{title} | |
</Text> | |
</FlexFull> | |
)} | |
<input | |
type="range" | |
value={sliderValue} // Bind slider value to the local state | |
min={0} | |
max={duration} | |
onChange={(e) => { | |
handleSeek(e); // Seek when the slider is moved | |
setSliderValue(Number(e.target.value)); // Keep sliderValue in sync | |
}} | |
className={`h-[0.6vh] w-full shadowNarrowNormal | |
appearance-none | |
focus:outline-black hover:cursor-pointer | |
[&::-moz-range-thumb]:size-[1.8vh] | |
[&::-moz-range-thumb]:appearance-none | |
${darkMode ? darkModeSlideStyle : lightModeSlideStyle} | |
[&::-webkit-slider-thumb]:shadowNarrowNormal | |
[&::-moz-range-thumb]:shadowNarrowNormal | |
[&::-moz-range-thumb]:hover:cursor-pointer | |
active:[&::-moz-range-thumb]:scale-110 | |
[&::-webkit-slider-thumb]:size-[1.8vh] | |
[&::-webkit-slider-thumb]:appearance-none | |
active:[&::-webkit-slider-thumb]:scale-110 | |
[&::-moz-range-thumb]:rounded-full | |
[&::-webkit-slider-thumb]:rounded-full | |
transition-300`} | |
/> | |
<FlexFull | |
className={`justify-between text-sm text-col-200 px-[1vh] pt-[0.5vh]`} | |
> | |
<Text | |
className={`text-[1.5vh] text-col-500 group-hover:text-col-pink transition-300 ${ | |
darkMode | |
? "text-col-500 textShadow" | |
: "text-col-900 font-semibold lightTextShadow" | |
} | |
hoverCursor="hover:cursor-pointer`} | |
> | |
{formatTime(currentTime)} | |
</Text> | |
<motion.button | |
className="hover:cursor-pointer" | |
onClick={togglePlayPause} | |
> | |
<HStack | |
className="items-center hover:cursor-pointer group" | |
gap="gap-[0.3vh]" | |
> | |
<Icon | |
icon={isPlaying ? FaStop : FaPlay} | |
iconClassName={`text-[1.3vh] text-col-500 transition-300 ${ | |
darkMode | |
? "text-col-500 textShadow group-hover:text-col-pink" | |
: "text-col-900 font-semibold lightTextShadow group-hover:text-indigo-800" | |
} | |
hoverCursor="hover:cursor-pointer`} | |
/> | |
<Text | |
className={`text-[1.6vh] transition-300 ${ | |
darkMode | |
? "text-col-500 textShadow group-hover:text-col-pink " | |
: "text-col-900 font-semibold lightTextShadow group-hover:text-indigo-800" | |
} | |
hoverCursor="hover:cursor-pointer`} | |
> | |
{isPlaying ? "stop" : "play"} | |
</Text> | |
</HStack> | |
</motion.button> | |
<Text | |
className={`text-[1.5vh] text-col-500 group-hover:text-col-pink transition-300 ${ | |
darkMode | |
? "text-col-500 textShadow" | |
: "text-col-900 font-semibold lightTextShadow" | |
} | |
hoverCursor="hover:cursor-pointer`} | |
> | |
{formatTime(duration)} | |
</Text> | |
</FlexFull> | |
</VStackFull> | |
</FlexFull> | |
)} | |
</> | |
); | |
} | |
function formatTime(time: number): string { | |
const minutes = Math.floor(time / 60); | |
const seconds = Math.floor(time % 60); | |
return `${minutes}:${seconds.toString().padStart(2, "0")}`; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment