-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Why: - podcast episode pages had the default browser audioplayer, which was not intended; How: - added a custom audioplayer for both desktop and mobile versions of the podcast episode page; ![imagem](https://github.com/user-attachments/assets/0c98e9f9-743a-4c1c-9759-111fbcfd2176) ![imagem](https://github.com/user-attachments/assets/2272b759-9f01-4040-905b-942c953eaeaf)
- Loading branch information
Showing
13 changed files
with
558 additions
and
144 deletions.
There are no files selected for viewing
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
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
123 changes: 123 additions & 0 deletions
123
src/app/_blocks/EpisodeHead/AudioPlayer/ButtonIcons/index.tsx
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
export function AudioPlayButton({ width = '80', height = '54' }) { | ||
return ( | ||
<svg | ||
width={width} | ||
height={height} | ||
viewBox="0 0 54 54" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<circle cx="27" cy="27" r="27" fill="white" /> | ||
<path | ||
d="M39.3659 26.1794C41.3602 27.2908 41.3675 28.6889 39.3659 29.9454L23.9 40.3759C21.9567 41.4129 20.6369 40.8006 20.4984 38.5567L20.4328 16.7258C20.389 14.6588 22.0916 14.0748 23.7141 15.0658L39.3659 26.1794Z" | ||
fill="#773BFF" | ||
/> | ||
</svg> | ||
) | ||
} | ||
|
||
export function AudioPauseButton({ width = '80', height = '54' }) { | ||
return ( | ||
<svg | ||
width={width} | ||
height={height} | ||
viewBox="0 0 54 54" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<circle cx="27" cy="27" r="27" fill="white" /> | ||
<path | ||
d="M19.6667 40C21.6833 40 23.3333 38.3286 23.3333 36.2857V17.7143C23.3333 15.6714 21.6833 14 19.6667 14C17.65 14 16 15.6714 16 17.7143V36.2857C16 38.3286 17.65 40 19.6667 40ZM30.6667 17.7143V36.2857C30.6667 38.3286 32.3167 40 34.3333 40C36.35 40 38 38.3286 38 36.2857V17.7143C38 15.6714 36.35 14 34.3333 14C32.3167 14 30.6667 15.6714 30.6667 17.7143Z" | ||
fill="#773BFF" | ||
/> | ||
</svg> | ||
) | ||
} | ||
|
||
export function MoveFifteenIcon({ width = '32', height = '32' }) { | ||
return ( | ||
<svg | ||
width={width} | ||
height={height} | ||
viewBox="0 0 20 20" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<path | ||
d="M15.4773 5C14.136 2.60879 11.629 1 8.75758 1C4.47319 1 1 4.58172 1 9C1 11.3894 2.01577 13.5341 3.62628 15M13.1212 6H17V2" | ||
stroke="currentColor" | ||
strokeWidth="2" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
/> | ||
<path | ||
d="M7.87706 18V11.28L6.08906 11.94V10.476L8.16506 9.6H9.53306V18H7.87706ZM14.1957 18.168C12.7197 18.168 11.3997 17.364 11.1837 15.768H12.7557C12.8997 16.392 13.4157 16.824 14.1957 16.824C15.1797 16.824 15.7797 16.164 15.7797 15.264C15.7797 14.412 15.1917 13.74 14.2437 13.74C13.5717 13.74 13.1037 14.04 12.8517 14.472H11.3277L12.1197 9.6H16.6197V10.92H13.4277L13.0797 12.936C13.4397 12.6 14.0037 12.408 14.6637 12.408C16.2237 12.408 17.3997 13.5 17.3997 15.216C17.3997 16.86 16.0797 18.168 14.1957 18.168Z" | ||
fill="currentColor" | ||
/> | ||
</svg> | ||
) | ||
} | ||
|
||
export function BackFifteenIcon({ width = '32', height = '32' }) { | ||
return ( | ||
<svg | ||
width={width} | ||
height={height} | ||
viewBox="0 0 20 20" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<path | ||
d="M3.52267 5C4.86399 2.60879 7.37103 1 10.2424 1C14.5268 1 18 4.58172 18 9C18 11.3894 16.9842 13.5341 15.3737 15M5.87879 6H2V2" | ||
stroke="currentColor" | ||
strokeWidth="2" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
/> | ||
<path | ||
d="M2.87706 18V11.28L1.08906 11.94V10.476L3.16506 9.6H4.53306V18H2.87706ZM9.19572 18.168C7.71972 18.168 6.39972 17.364 6.18372 15.768H7.75572C7.89972 16.392 8.41572 16.824 9.19572 16.824C10.1797 16.824 10.7797 16.164 10.7797 15.264C10.7797 14.412 10.1917 13.74 9.24372 13.74C8.57172 13.74 8.10372 14.04 7.85172 14.472H6.32772L7.11972 9.6H11.6197V10.92H8.42772L8.07972 12.936C8.43972 12.6 9.00372 12.408 9.66372 12.408C11.2237 12.408 12.3997 13.5 12.3997 15.216C12.3997 16.86 11.0797 18.168 9.19572 18.168Z" | ||
fill="currentColor" | ||
/> | ||
</svg> | ||
) | ||
} | ||
|
||
export function RaiseVolumeIcon({ width = '50', height = '40' }) { | ||
return ( | ||
<svg | ||
width={width} | ||
height={height} | ||
viewBox="0 0 24 26" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<path | ||
d="M19.6824 8.07941C21.2661 10.5939 21.4898 13.9404 19.9833 16.9018M16.3459 9.72406C17.6827 11.2866 17.8715 13.3662 16.5999 15.2065M12 6L7.58775 9.4884H3V14.5111L7.58775 14.5099L12 18V6Z" | ||
stroke="currentColor" | ||
strokeWidth="2" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
/> | ||
</svg> | ||
) | ||
} | ||
|
||
export function MuteIcon({ width = '50', height = '40' }) { | ||
return ( | ||
<svg | ||
width={width} | ||
height={height} | ||
viewBox="0 0 24 26" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<path | ||
d="M16.8827 8.5V4L12.2315 8.65121L5.5 8.65121V15.3481H9.54584M16.8827 13V20L13 16.1173M6 18.5L9.54584 15.3481M19.5 6.5L9.54584 15.3481" | ||
stroke="currentColor" | ||
strokeWidth="2" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
/> | ||
</svg> | ||
) | ||
} |
9 changes: 9 additions & 0 deletions
9
src/app/_blocks/EpisodeHead/AudioPlayer/Utilities/formatDuration.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export default function formatDuration(duration: number) { | ||
const minutes = Math.floor(duration / 60) | ||
const remainingSeconds = Math.floor(duration % 60) | ||
|
||
const formattedMinutes = String(minutes).padStart(2, '0') | ||
const formattedSeconds = String(remainingSeconds).padStart(2, '0') | ||
|
||
return `${formattedMinutes}:${formattedSeconds}` | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,153 @@ | ||
'use client' | ||
|
||
const AudioPlayer: React.FC<{ src: string; type: string }> = ({ src, type }) => { | ||
import { useEffect, useRef, useState } from 'react' | ||
|
||
import { | ||
AudioPauseButton, | ||
AudioPlayButton, | ||
BackFifteenIcon, | ||
MoveFifteenIcon, | ||
MuteIcon, | ||
RaiseVolumeIcon, | ||
} from './ButtonIcons' | ||
import styles from './styles.module.css' | ||
import formatDuration from './Utilities/formatDuration' | ||
|
||
export default function AudioPlayer({ src }: { src: string }) { | ||
const [isPlaying, setIsPlaying] = useState(false) | ||
const [currentTime, setCurrentTime] = useState(0) | ||
const [duration, setDuration] = useState(0) | ||
const [isMuted, setIsMuted] = useState(false) | ||
|
||
const audioRef = useRef<HTMLAudioElement | null>(null) | ||
|
||
useEffect(() => { | ||
const audio = new Audio(src) | ||
audioRef.current = audio | ||
|
||
const handleLoadedMetadata = () => setDuration(audio.duration) | ||
const handleTimeUpdate = () => setCurrentTime(audio.currentTime) | ||
|
||
audio.addEventListener('loadedmetadata', handleLoadedMetadata) | ||
audio.addEventListener('timeupdate', handleTimeUpdate) | ||
|
||
return () => { | ||
audio.removeEventListener('loadedmetadata', handleLoadedMetadata) | ||
audio.removeEventListener('timeupdate', handleTimeUpdate) | ||
audio.pause() | ||
audioRef.current = null | ||
} | ||
}, [src]) | ||
|
||
const togglePlayPause = () => { | ||
if (audioRef.current) { | ||
isPlaying ? audioRef.current.pause() : audioRef.current.play() | ||
setIsPlaying(!isPlaying) | ||
} | ||
} | ||
|
||
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
const time = Number(e.target.value) | ||
if (audioRef.current) { | ||
audioRef.current.currentTime = time | ||
setCurrentTime(time) | ||
} | ||
} | ||
|
||
const skip = (seconds: number) => { | ||
if (audioRef.current) { | ||
audioRef.current.currentTime += seconds | ||
} | ||
} | ||
|
||
const toggleMute = () => { | ||
if (audioRef.current) { | ||
audioRef.current.muted = !isMuted | ||
setIsMuted(!isMuted) | ||
} | ||
} | ||
|
||
// Utility to generate the dynamic gradient for the progress bar | ||
const progressBarStyle = { | ||
'--dynamic-gradient': `linear-gradient(to right, var(--dark-rock-800) ${ | ||
(currentTime / duration) * 100 | ||
}%, var(--soft-white-100) 0%)`, | ||
} | ||
|
||
// Reusable Button components to reduce duplication | ||
const PlayPauseButton = ({ className, width }: { className?: string; width?: string }) => ( | ||
<button className={className} onClick={togglePlayPause}> | ||
{isPlaying ? <AudioPauseButton width={width} /> : <AudioPlayButton width={width} />} | ||
</button> | ||
) | ||
|
||
const MuteUnmuteButton = ({ width }: { width: string }) => ( | ||
<button onClick={toggleMute}> | ||
{isMuted ? <MuteIcon width={width} /> : <RaiseVolumeIcon width={width} />} | ||
</button> | ||
) | ||
|
||
const SkipButton = ({ | ||
seconds, | ||
Icon, | ||
width, | ||
}: { | ||
seconds: number | ||
Icon: React.FC<any> | ||
width: string | ||
}) => ( | ||
<button onClick={() => skip(seconds)}> | ||
<Icon width={width} /> | ||
</button> | ||
) | ||
|
||
return ( | ||
<div style={{ width: '100%' }}> | ||
<audio controls style={{ width: '100%' }}> | ||
<source src={src} type={type} /> | ||
Your browser does not support the audio element. | ||
</audio> | ||
<div> | ||
{/* Desktop Player */} | ||
<div className={styles.desktopAudioPlayer}> | ||
<PlayPauseButton width={'120px'} /> | ||
<MuteUnmuteButton width={'120px'} /> | ||
<SkipButton seconds={-15} Icon={BackFifteenIcon} width={'50px'} /> | ||
<div className={styles.duration}> | ||
<span> | ||
{formatDuration(currentTime)} / {formatDuration(duration)} | ||
</span> | ||
</div> | ||
<input | ||
className={styles.progressBar} | ||
style={progressBarStyle} | ||
type="range" | ||
min={0} | ||
max={duration} | ||
value={currentTime} | ||
onChange={handleSeek} | ||
/> | ||
<SkipButton seconds={15} Icon={MoveFifteenIcon} width={'50px'} /> | ||
</div> | ||
|
||
{/* Mobile Player */} | ||
<div className={styles.mobileAudioPlayer}> | ||
<PlayPauseButton className={styles.playPauseButton} width={'50px'} /> | ||
<div className={styles.duration}> | ||
<span> | ||
{formatDuration(currentTime)} / {formatDuration(duration)} | ||
</span> | ||
</div> | ||
<input | ||
className={styles.progressBar} | ||
style={progressBarStyle} | ||
type="range" | ||
min={0} | ||
max={duration} | ||
value={currentTime} | ||
onChange={handleSeek} | ||
/> | ||
<div className={styles.mobileButtonContainer}> | ||
<MuteUnmuteButton width={'28px'} /> | ||
<SkipButton seconds={-15} Icon={BackFifteenIcon} width={'20px'} /> | ||
<SkipButton seconds={15} Icon={MoveFifteenIcon} width={'20px'} /> | ||
</div> | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
export default AudioPlayer |
Oops, something went wrong.