import { useEffect, useRef, useState } from 'react' import { sendEvent } from '../analytics' interface EpisodeAudioPlayerProps { src: string title?: string size?: 'full' | 'compact' spotifyUrl?: string } function formatTime(seconds: number): string { if (!isFinite(seconds) || seconds < 0) return '0:00' const m = Math.floor(seconds / 60) const s = Math.floor(seconds % 60) return `${m}:${String(s).padStart(2, '0')}` } function SpotifyLinkIcon({ href }: { href: string }) { return ( ) } export function EpisodeAudioPlayer({ src, title, size = 'full', spotifyUrl }: EpisodeAudioPlayerProps) { const audioRef = useRef(null) const [playing, setPlaying] = useState(false) const [buffering, setBuffering] = useState(false) const [currentTime, setCurrentTime] = useState(0) const [duration, setDuration] = useState(0) const playTrackedRef = useRef(false) const listenStartRef = useRef(null) const totalListenSecondsRef = useRef(0) function flushListenTime() { if (listenStartRef.current !== null && title) { const seconds = Math.round((Date.now() - listenStartRef.current) / 1000) if (seconds > 1) { totalListenSecondsRef.current += seconds sendEvent('audio_listen_time', { title, seconds }) } listenStartRef.current = null } } useEffect(() => { const audio = audioRef.current if (!audio) return const onTimeUpdate = () => setCurrentTime(audio.currentTime) const onDurationChange = () => setDuration(audio.duration) const onEnded = () => { setPlaying(false) setBuffering(false) flushListenTime() if (title) sendEvent('audio_completion', { title }) } const onWaiting = () => setBuffering(true) const onCanPlay = () => setBuffering(false) audio.addEventListener('timeupdate', onTimeUpdate) audio.addEventListener('durationchange', onDurationChange) audio.addEventListener('loadedmetadata', onDurationChange) audio.addEventListener('ended', onEnded) audio.addEventListener('waiting', onWaiting) audio.addEventListener('canplay', onCanPlay) return () => { audio.removeEventListener('timeupdate', onTimeUpdate) audio.removeEventListener('durationchange', onDurationChange) audio.removeEventListener('loadedmetadata', onDurationChange) audio.removeEventListener('ended', onEnded) audio.removeEventListener('waiting', onWaiting) audio.removeEventListener('canplay', onCanPlay) flushListenTime() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [src]) function togglePlay() { const audio = audioRef.current if (!audio) return if (playing) { audio.pause() setPlaying(false) flushListenTime() if (title) sendEvent('audio_pause', { title }) } else { audio.play().then(() => { setPlaying(true) listenStartRef.current = Date.now() if (!playTrackedRef.current && title) { playTrackedRef.current = true fetch('/api/analytics/play', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }), }).catch(() => {}) } }).catch(() => {}) } } function seek(e: React.MouseEvent) { const audio = audioRef.current if (!audio || !duration) return const rect = e.currentTarget.getBoundingClientRect() const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)) audio.currentTime = pct * duration setCurrentTime(pct * duration) } const pct = duration > 0 ? (currentTime / duration) * 100 : 0 const isCompact = size === 'compact' return ( {!isCompact && ( Verse by Verse with Nate {spotifyUrl && } )} {title && ( {title} )} {playing ? : } { const audio = audioRef.current if (!audio) return if (e.key === 'ArrowRight') audio.currentTime = Math.min(duration, audio.currentTime + 10) if (e.key === 'ArrowLeft') audio.currentTime = Math.max(0, audio.currentTime - 10) }} > {formatTime(currentTime)} {formatTime(duration)} {isCompact && spotifyUrl && } ) }
{title}