"use client"; import { useState, useEffect, useRef } from "react"; import Head from "next/head"; import Link from "next/link"; import { format } from "date-fns"; import { PodcastEpisode, DEFAULT_CONFIG } from "@/lib/podcast"; interface EpisodeWithAudio extends PodcastEpisode { audioUrl: string; audioDuration: number; } function formatDuration(seconds: number): string { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, "0")}`; } export default function PodcastPage() { const [episodes, setEpisodes] = useState([]); const [loading, setLoading] = useState(true); const [currentEpisode, setCurrentEpisode] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [progress, setProgress] = useState(0); const [duration, setDuration] = useState(0); const audioRef = useRef(null); useEffect(() => { fetchEpisodes(); }, []); useEffect(() => { if (currentEpisode && audioRef.current) { audioRef.current.play(); setIsPlaying(true); } }, [currentEpisode]); async function fetchEpisodes() { try { const res = await fetch("/api/messages"); const data = await res.json(); // Filter to only episodes with audio const episodesWithAudio = (data || []) .filter((m: any) => m.audio_url) .map((m: any) => ({ id: m.id, title: extractTitle(m.content), description: extractExcerpt(m.content), content: m.content, date: m.date, timestamp: m.timestamp, audioUrl: m.audio_url, audioDuration: m.audio_duration || 300, tags: m.tags || [], })) .sort((a: any, b: any) => b.timestamp - a.timestamp); setEpisodes(episodesWithAudio); } catch (err) { console.error("Failed to fetch episodes:", err); } finally { setLoading(false); } } function extractTitle(content: string): string { const lines = content.split("\n"); const titleLine = lines.find((l) => l.startsWith("# ") || l.startsWith("## ")); return titleLine?.replace(/#{1,2}\s/, "").trim() || "Daily Digest"; } function extractExcerpt(content: string, maxLength: number = 150): string { const plainText = content .replace(/#{1,6}\s/g, "") .replace(/(\*\*|__|\*|_)/g, "") .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") .replace(/```[\s\S]*?```/g, "") .replace(/`([^`]+)`/g, " $1 ") .replace(/\n+/g, " ") .trim(); if (plainText.length <= maxLength) return plainText; return plainText.substring(0, maxLength).trim() + "..."; } function handlePlay(episode: EpisodeWithAudio) { if (currentEpisode?.id === episode.id) { togglePlay(); } else { setCurrentEpisode(episode); setProgress(0); } } function togglePlay() { if (audioRef.current) { if (isPlaying) { audioRef.current.pause(); } else { audioRef.current.play(); } setIsPlaying(!isPlaying); } } function handleTimeUpdate() { if (audioRef.current) { setProgress(audioRef.current.currentTime); setDuration(audioRef.current.duration || currentEpisode?.audioDuration || 0); } } function handleSeek(e: React.ChangeEvent) { const newTime = parseFloat(e.target.value); if (audioRef.current) { audioRef.current.currentTime = newTime; setProgress(newTime); } } function handleEnded() { setIsPlaying(false); setProgress(0); } const rssUrl = "https://blog-backup-two.vercel.app/api/podcast/rss"; return ( <> Podcast | OpenClaw Daily Digest
{/* Header */}
๐ŸŽง
Daily Digest Podcast
{/* Podcast Header */}
๐ŸŽ™๏ธ

{DEFAULT_CONFIG.title}

{DEFAULT_CONFIG.description}

{/* Now Playing */} {currentEpisode && (

{currentEpisode.title}

{format(new Date(currentEpisode.date), "MMMM d, yyyy")}

{formatDuration(Math.floor(progress))} / {formatDuration(currentEpisode.audioDuration)}
)} {/* Episode List */}

Episodes

{loading ? (
) : episodes.length === 0 ? (

No podcast episodes yet.

Episodes are generated when new digests are posted with audio enabled.

) : ( episodes.map((episode) => (

{episode.title}

{episode.description}

{format(new Date(episode.date), "MMMM d, yyyy")} ยท {formatDuration(episode.audioDuration)} {episode.tags && episode.tags.length > 0 && ( <> ยท {episode.tags.slice(0, 2).join(", ")} )}
)) )}
{/* Subscribe Section */}

Subscribe to the Podcast

Get the latest tech digest delivered to your favorite podcast app.

{rssUrl}
{/* Footer */}
); }