PodcastDetail Component
The PodcastDetail component displays comprehensive information about a single podcast episode, including title, description, publication date, YouTube video embed, and action buttons for playback, downloading, marking favorites, and more.
Location
src/components/PodcastDetail/PodcastDetail.jsx
Import
import PodcastDetail from "../PodcastDetail/PodcastDetail" ;
Props
Callback function triggered when the play/pause button is clicked. Receives the podcast object.
Features
YouTube Integration
Automatically fetches and embeds the corresponding YouTube video:
const YT_API_KEY = process . env . REACT_APP_YT_API_KEY ;
const CHANNEL_ID = process . env . REACT_APP_CHANNEL_ID ;
const fetchYoutubeVideo = async () => {
if ( foundPodcast ) {
try {
const response = await fetch (
`https://www.googleapis.com/youtube/v3/search?part=snippet&q= ${ encodeURIComponent (
foundPodcast . title
) } &key= ${ YT_API_KEY } &channelId= ${ CHANNEL_ID } &type=video&maxResults=1`
);
const data = await response . json ();
if ( data . items && data . items . length > 0 ) {
setYoutubeVideoId ( data . items [ 0 ]. id . videoId );
}
} catch ( error ) {
console . error ( "Error fetching YouTube video:" , error );
}
}
};
Requires REACT_APP_YT_API_KEY and REACT_APP_CHANNEL_ID environment variables.
Episode Status Tracking
The component tracks multiple episode states:
Started
Completed
Favorite
Listen Later
Episodes with saved playback time greater than 0 const isListened =
listenedEpisodes . includes ( podcast . title ) ||
( playbackTimes [ podcast . title ] && playbackTimes [ podcast . title ] > 0 );
Episodes marked as completed by the user const isCompleted = completedEpisodes . includes ( podcast . title );
Episodes added to favorites const isFavorite = favoriteEpisodes . includes ( podcast . title );
Episodes saved for later listening const isListenLater = listenLaterEpisodes . includes ( podcast . title );
The component provides multiple action buttons:
Play/Pause Toggle playback of the episode. Automatically removes from completed if replayed.
Favorite Add or remove episode from favorites list
Listen Later Save episode for later listening
Mark Completed Manually mark episode as completed or remove from completed
Share Share episode using Web Share API (mobile devices)
Download Download episode MP3 file with progress indicator
Download Functionality
Uses custom useDownload hook with progress tracking:
const { isLoading , progress , isCancelled , handleDownload , cancelDownload } = useDownload ();
< motion.button
className = { styles . actionButton }
onClick = {
isLoading
? cancelDownload
: () => handleDownload ( podcast . audio , podcast . title )
}
disabled = { isLoading && isCancelled }
style = { {
backgroundColor: isLoading ? "#0f3460" : "" ,
color: isLoading ? "#16db93" : ""
} }
>
{ isLoading ? (
isCancelled ? (
< span > Descarga cancelada </ span >
) : (
<>
< FidgetSpinner { ... spinnerProps } />
< span > Descargando { progress } % </ span >
< Close onClick = { cancelDownload } />
</>
)
) : (
<>
< Download />
Descargar
</>
) }
</ motion.button >
Confirmation Dialogs
Critical actions show confirmation toasts:
const showConfirmToast = ( message , onConfirm ) => {
toast . custom (
( t ) => (
< div className = { styles . confirmToast } >
< div className = { styles . confirmHeader } >
< Warning className = { styles . warningIcon } />
< h3 > Confirmar Acción </ h3 >
</ div >
< p className = { styles . confirmMessage } > { message } </ p >
< div className = { styles . confirmButtons } >
< motion.button
className = { styles . confirmButton }
onClick = { () => {
toast . dismiss ( t . id );
onConfirm ();
} }
>
Confirmar
</ motion.button >
< motion.button
className = { styles . cancelButton }
onClick = { () => toast . dismiss ( t . id ) }
>
Cancelar
</ motion.button >
</ div >
</ div >
),
{
duration: Infinity ,
position: "top-center" ,
className: styles . customToast ,
closeButton: false ,
closeOnClick: false ,
draggable: false
}
);
};
URL Routing
Accesses episode via URL parameter:
const { id } = useParams (); // e.g., "nadie-sabe-nada-programa-123"
const foundPodcast = songs . find (( song ) => slugify ( song . title ) === id );
If podcast not found, redirects to 404:
if ( ! foundPodcast ) {
navigate ( "/404" );
}
Status Indicators
Dynamic status display based on episode state:
const getStatusIcon = () => {
if ( isPodcastPlaying ) return < Headphones /> ;
if ( isCompleted ) return < CheckCircle /> ;
if ( isListened ) return < Headphones /> ;
return < HeadsetOff /> ;
};
const getStatusText = () => {
if ( isPodcastPlaying ) return "Reproduciendo" ;
if ( isCompleted ) return "Completado" ;
if ( isListened ) return "Empezado" ;
return "No Empezado" ;
};
Displays playback time in MM:SS format:
const formatTime = ( seconds ) => {
if ( ! seconds ) return "0:00" ;
const minutes = Math . floor ( seconds / 60 );
const remainingSeconds = Math . floor ( seconds % 60 );
return ` ${ minutes } : ${ remainingSeconds . toString (). padStart ( 2 , "0" ) } ` ;
};
Redux Dispatches
Toggle Favorite
Toggle Listen Later
Mark Completed
Remove Started
const handleFavorites = () => {
dispatch ( toggleFavorite ( podcast ));
};
YouTube Player
Embeds YouTube video when available:
{ youtubeVideoId && (
< motion.div
className = { styles . youtubePlayer }
initial = { { opacity: 0 , scale: 0.9 } }
animate = { { opacity: 1 , scale: 1 } }
transition = { { delay: 0.4 , duration: 0.5 } }
>
< YouTube
videoId = { youtubeVideoId }
opts = { {
height: "390" ,
width: "640" ,
playerVars: {
autoplay: 0
},
modestbranding: 1 ,
showinfo: 0
} }
onPlay = { () => dispatch ( togglePlay ( false )) }
/>
</ motion.div >
)}
When YouTube video starts playing, it pauses the audio player to prevent conflicts.
< Helmet >
< title > { podcast . title } - Nadie Sabe Nada Podcast </ title >
</ Helmet >
Usage Example
import PodcastDetail from "./components/PodcastDetail/PodcastDetail" ;
import { useDispatch } from "react-redux" ;
import { setCurrentPodcast , togglePlay } from "./store/slices/playerSlice" ;
function PodcastDetailPage () {
const dispatch = useDispatch ();
const handlePlay = ( podcast ) => {
dispatch ( setCurrentPodcast ( podcast ));
dispatch ( togglePlay ( true ));
};
return < PodcastDetail onPlayPodcast = { handlePlay } /> ;
}