Skip to main content

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

onPlayPodcast
function
required
Callback function triggered when the play/pause button is clicked. Receives the podcast object.
(podcast) => void

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:
Episodes with saved playback time greater than 0
const isListened =
  listenedEpisodes.includes(podcast.title) ||
  (playbackTimes[podcast.title] && playbackTimes[podcast.title] > 0);

Action Buttons

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";
};

Time Formatting

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

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.

SEO Metadata

<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} />;
}

Build docs developers (and LLMs) love