From f2602e9e6b9c065b0054de4a5e23855b112d60fa Mon Sep 17 00:00:00 2001 From: ritikprajapat21 Date: Tue, 8 Jul 2025 12:27:02 +0530 Subject: [PATCH] Fix issue #152: Added close functionality to player Added closePlayer function and an X button to player. The function cleans all the state variable responsible for playing the audio. --- .../podcasts/podcasts-client.tsx | 1936 +++++++++-------- 1 file changed, 1035 insertions(+), 901 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx index 5489d86..d982804 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx @@ -1,968 +1,1102 @@ -'use client'; +"use client"; -import { format } from 'date-fns'; -import { AnimatePresence, motion } from 'framer-motion'; +import { format } from "date-fns"; +import { AnimatePresence, motion } from "framer-motion"; import { - Calendar, - MoreHorizontal, - Pause, - Play, - Podcast, - Search, - SkipBack, - SkipForward, - Trash2, - Volume2, VolumeX -} from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; + Calendar, + MoreHorizontal, + Pause, + Play, + Podcast, + Search, + SkipBack, + SkipForward, + Trash2, + Volume2, + VolumeX, + X, +} from "lucide-react"; +import { useEffect, useRef, useState } from "react"; // UI Components -import { Button } from '@/components/ui/button'; -import { Card } from '@/components/ui/card'; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu'; -import { Input } from '@/components/ui/input'; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; -import { Slider } from '@/components/ui/slider'; +import { Slider } from "@/components/ui/slider"; import { toast } from "sonner"; interface PodcastItem { - id: number; - title: string; - created_at: string; - file_location: string; - podcast_transcript: any[]; - search_space_id: number; + id: number; + title: string; + created_at: string; + file_location: string; + podcast_transcript: any[]; + search_space_id: number; } interface PodcastsPageClientProps { - searchSpaceId: string; + searchSpaceId: string; } const pageVariants = { - initial: { opacity: 0 }, - enter: { opacity: 1, transition: { duration: 0.4, ease: 'easeInOut', staggerChildren: 0.1 } }, - exit: { opacity: 0, transition: { duration: 0.3, ease: 'easeInOut' } } + initial: { opacity: 0 }, + enter: { + opacity: 1, + transition: { duration: 0.4, ease: "easeInOut", staggerChildren: 0.1 }, + }, + exit: { opacity: 0, transition: { duration: 0.3, ease: "easeInOut" } }, }; const podcastCardVariants = { - initial: { scale: 0.95, y: 20, opacity: 0 }, - animate: { scale: 1, y: 0, opacity: 1, transition: { type: "spring", stiffness: 300, damping: 25 } }, - exit: { scale: 0.95, y: -20, opacity: 0 }, - hover: { y: -5, scale: 1.02, transition: { duration: 0.2 } } + initial: { scale: 0.95, y: 20, opacity: 0 }, + animate: { + scale: 1, + y: 0, + opacity: 1, + transition: { type: "spring", stiffness: 300, damping: 25 }, + }, + exit: { scale: 0.95, y: -20, opacity: 0 }, + hover: { y: -5, scale: 1.02, transition: { duration: 0.2 } }, }; const MotionCard = motion(Card); -export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClientProps) { - const [podcasts, setPodcasts] = useState([]); - const [filteredPodcasts, setFilteredPodcasts] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); - const [sortOrder, setSortOrder] = useState('newest'); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [podcastToDelete, setPodcastToDelete] = useState<{ id: number, title: string } | null>(null); - const [isDeleting, setIsDeleting] = useState(false); - - // Audio player state - const [currentPodcast, setCurrentPodcast] = useState(null); - const [audioSrc, setAudioSrc] = useState(undefined); - const [isAudioLoading, setIsAudioLoading] = useState(false); - const [isPlaying, setIsPlaying] = useState(false); - const [currentTime, setCurrentTime] = useState(0); - const [duration, setDuration] = useState(0); - const [volume, setVolume] = useState(0.7); - const [isMuted, setIsMuted] = useState(false); - const audioRef = useRef(null); - const currentObjectUrlRef = useRef(null); +export default function PodcastsPageClient({ + searchSpaceId, +}: PodcastsPageClientProps) { + const [podcasts, setPodcasts] = useState([]); + const [filteredPodcasts, setFilteredPodcasts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [sortOrder, setSortOrder] = useState("newest"); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [podcastToDelete, setPodcastToDelete] = useState<{ + id: number; + title: string; + } | null>(null); + const [isDeleting, setIsDeleting] = useState(false); - // Add podcast image URL constant - const PODCAST_IMAGE_URL = "https://static.vecteezy.com/system/resources/thumbnails/002/157/611/small_2x/illustrations-concept-design-podcast-channel-free-vector.jpg"; + // Audio player state + const [currentPodcast, setCurrentPodcast] = useState( + null, + ); + const [audioSrc, setAudioSrc] = useState(undefined); + const [isAudioLoading, setIsAudioLoading] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [volume, setVolume] = useState(0.7); + const [isMuted, setIsMuted] = useState(false); + const audioRef = useRef(null); + const currentObjectUrlRef = useRef(null); - // Fetch podcasts from API - useEffect(() => { - const fetchPodcasts = async () => { - try { - setIsLoading(true); - - // Get token from localStorage - const token = localStorage.getItem('surfsense_bearer_token'); - - if (!token) { - setError('Authentication token not found. Please log in again.'); - setIsLoading(false); - return; - } + // Add podcast image URL constant + const PODCAST_IMAGE_URL = + "https://static.vecteezy.com/system/resources/thumbnails/002/157/611/small_2x/illustrations-concept-design-podcast-channel-free-vector.jpg"; - // Fetch all podcasts for this search space - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/`, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - cache: 'no-store', - } - ); + // Fetch podcasts from API + useEffect(() => { + const fetchPodcasts = async () => { + try { + setIsLoading(true); - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new Error(`Failed to fetch podcasts: ${response.status} ${errorData?.detail || ''}`); - } + // Get token from localStorage + const token = localStorage.getItem("surfsense_bearer_token"); - const data: PodcastItem[] = await response.json(); - setPodcasts(data); - setFilteredPodcasts(data); - setError(null); - } catch (error) { - console.error('Error fetching podcasts:', error); - setError(error instanceof Error ? error.message : 'Unknown error occurred'); - setPodcasts([]); - setFilteredPodcasts([]); - } finally { - setIsLoading(false); - } - }; + if (!token) { + setError("Authentication token not found. Please log in again."); + setIsLoading(false); + return; + } - fetchPodcasts(); - }, [searchSpaceId]); + // Fetch all podcasts for this search space + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/`, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + cache: "no-store", + }, + ); - // Filter and sort podcasts based on search query and sort order - useEffect(() => { - let result = [...podcasts]; - - // Filter by search term - if (searchQuery) { - const query = searchQuery.toLowerCase(); - result = result.filter(podcast => - podcast.title.toLowerCase().includes(query) - ); - } - - // Filter by search space - result = result.filter(podcast => - podcast.search_space_id === parseInt(searchSpaceId) - ); - - // Sort podcasts - result.sort((a, b) => { - const dateA = new Date(a.created_at).getTime(); - const dateB = new Date(b.created_at).getTime(); - - return sortOrder === 'newest' ? dateB - dateA : dateA - dateB; - }); - - setFilteredPodcasts(result); - }, [podcasts, searchQuery, sortOrder, searchSpaceId]); + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error( + `Failed to fetch podcasts: ${response.status} ${errorData?.detail || ""}`, + ); + } - // Cleanup object URL on unmount or when currentPodcast changes - useEffect(() => { - return () => { - if (currentObjectUrlRef.current) { - URL.revokeObjectURL(currentObjectUrlRef.current); - currentObjectUrlRef.current = null; - } - }; - }, []); + const data: PodcastItem[] = await response.json(); + setPodcasts(data); + setFilteredPodcasts(data); + setError(null); + } catch (error) { + console.error("Error fetching podcasts:", error); + setError( + error instanceof Error ? error.message : "Unknown error occurred", + ); + setPodcasts([]); + setFilteredPodcasts([]); + } finally { + setIsLoading(false); + } + }; - // Audio player time update handler - const handleTimeUpdate = () => { - if (audioRef.current) { - setCurrentTime(audioRef.current.currentTime); - } - }; + fetchPodcasts(); + }, [searchSpaceId]); - // Audio player metadata loaded handler - const handleMetadataLoaded = () => { - if (audioRef.current) { - setDuration(audioRef.current.duration); - } - }; + // Filter and sort podcasts based on search query and sort order + useEffect(() => { + let result = [...podcasts]; - // Play/pause toggle - const togglePlayPause = () => { - if (audioRef.current) { - if (isPlaying) { - audioRef.current.pause(); - } else { - audioRef.current.play(); - } - setIsPlaying(!isPlaying); - } - }; + // Filter by search term + if (searchQuery) { + const query = searchQuery.toLowerCase(); + result = result.filter((podcast) => + podcast.title.toLowerCase().includes(query), + ); + } - // Seek to position - const handleSeek = (value: number[]) => { - if (audioRef.current) { - audioRef.current.currentTime = value[0]; - setCurrentTime(value[0]); - } - }; + // Filter by search space + result = result.filter( + (podcast) => podcast.search_space_id === parseInt(searchSpaceId), + ); - // Volume change - const handleVolumeChange = (value: number[]) => { - if (audioRef.current) { - const newVolume = value[0]; - - // Set volume - audioRef.current.volume = newVolume; - setVolume(newVolume); - - // Handle mute state based on volume - if (newVolume === 0) { - audioRef.current.muted = true; - setIsMuted(true); - } else { - audioRef.current.muted = false; - setIsMuted(false); - } - } - }; + // Sort podcasts + result.sort((a, b) => { + const dateA = new Date(a.created_at).getTime(); + const dateB = new Date(b.created_at).getTime(); - // Toggle mute - const toggleMute = () => { - if (audioRef.current) { - const newMutedState = !isMuted; - audioRef.current.muted = newMutedState; - setIsMuted(newMutedState); - - // If unmuting, restore previous volume if it was 0 - if (!newMutedState && volume === 0) { - const restoredVolume = 0.5; - audioRef.current.volume = restoredVolume; - setVolume(restoredVolume); - } - } - }; + return sortOrder === "newest" ? dateB - dateA : dateA - dateB; + }); - // Skip forward 10 seconds - const skipForward = () => { - if (audioRef.current) { - audioRef.current.currentTime = Math.min(audioRef.current.duration, audioRef.current.currentTime + 10); - } - }; + setFilteredPodcasts(result); + }, [podcasts, searchQuery, sortOrder, searchSpaceId]); - // Skip backward 10 seconds - const skipBackward = () => { - if (audioRef.current) { - audioRef.current.currentTime = Math.max(0, audioRef.current.currentTime - 10); - } - }; + // Cleanup object URL on unmount or when currentPodcast changes + useEffect(() => { + return () => { + if (currentObjectUrlRef.current) { + URL.revokeObjectURL(currentObjectUrlRef.current); + currentObjectUrlRef.current = null; + } + }; + }, []); - // Format time in MM:SS - const formatTime = (time: number) => { - const minutes = Math.floor(time / 60); - const seconds = Math.floor(time % 60); - return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; - }; + // Audio player time update handler + const handleTimeUpdate = () => { + if (audioRef.current) { + setCurrentTime(audioRef.current.currentTime); + } + }; - // Play podcast - Fetch blob and set object URL - const playPodcast = async (podcast: PodcastItem) => { - // If the same podcast is selected, just toggle play/pause - if (currentPodcast && currentPodcast.id === podcast.id) { - togglePlayPause(); - return; - } + // Audio player metadata loaded handler + const handleMetadataLoaded = () => { + if (audioRef.current) { + setDuration(audioRef.current.duration); + } + }; - // Prevent multiple simultaneous loading requests - if (isAudioLoading) { - return; - } - - try { - // Reset player state and show loading - setCurrentPodcast(podcast); - setAudioSrc(undefined); - setCurrentTime(0); - setDuration(0); - setIsPlaying(false); - setIsAudioLoading(true); - - const token = localStorage.getItem('surfsense_bearer_token'); - if (!token) { - throw new Error('Authentication token not found.'); - } + // Play/pause toggle + const togglePlayPause = () => { + if (audioRef.current) { + if (isPlaying) { + audioRef.current.pause(); + } else { + audioRef.current.play(); + } + setIsPlaying(!isPlaying); + } + }; - // Revoke previous object URL if exists (only after we've started the new request) - if (currentObjectUrlRef.current) { - URL.revokeObjectURL(currentObjectUrlRef.current); - currentObjectUrlRef.current = null; - } + // To close player + const closePlayer = () => { + if (isPlaying) { + audioRef.current?.pause(); + } + setIsPlaying(false); + setAudioSrc(undefined); + setCurrentTime(0); + setCurrentPodcast(null); + }; - // Use AbortController to handle timeout or cancellation - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout + // Seek to position + const handleSeek = (value: number[]) => { + if (audioRef.current) { + audioRef.current.currentTime = value[0]; + setCurrentTime(value[0]); + } + }; - try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcast.id}/stream`, - { - headers: { - 'Authorization': `Bearer ${token}`, - }, - signal: controller.signal - } - ); + // Volume change + const handleVolumeChange = (value: number[]) => { + if (audioRef.current) { + const newVolume = value[0]; - if (!response.ok) { - throw new Error(`Failed to fetch audio stream: ${response.statusText}`); - } + // Set volume + audioRef.current.volume = newVolume; + setVolume(newVolume); - const blob = await response.blob(); - const objectUrl = URL.createObjectURL(blob); - currentObjectUrlRef.current = objectUrl; - - // Set audio source - setAudioSrc(objectUrl); - - // Wait for the audio to be ready before playing - // We'll handle actual playback in the onLoadedData event instead of here - } catch (error) { - if (error instanceof DOMException && error.name === 'AbortError') { - throw new Error('Request timed out. Please try again.'); - } - throw error; - } finally { - clearTimeout(timeoutId); - } - } catch (error) { - console.error('Error fetching or playing podcast:', error); - toast.error(error instanceof Error ? error.message : 'Failed to load podcast audio.'); - // Reset state on error - setCurrentPodcast(null); - setAudioSrc(undefined); - } finally { - setIsAudioLoading(false); - } - }; + // Handle mute state based on volume + if (newVolume === 0) { + audioRef.current.muted = true; + setIsMuted(true); + } else { + audioRef.current.muted = false; + setIsMuted(false); + } + } + }; - // Function to handle podcast deletion - const handleDeletePodcast = async () => { - if (!podcastToDelete) return; - - setIsDeleting(true); - try { - const token = localStorage.getItem('surfsense_bearer_token'); - if (!token) { - setIsDeleting(false); - return; - } - - const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcastToDelete.id}`, { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - } - }); - - if (!response.ok) { - throw new Error(`Failed to delete podcast: ${response.statusText}`); - } - - // Close dialog and refresh podcasts - setDeleteDialogOpen(false); - setPodcastToDelete(null); - - // Update local state by removing the deleted podcast - setPodcasts(prevPodcasts => prevPodcasts.filter(podcast => podcast.id !== podcastToDelete.id)); - - // If the current playing podcast is deleted, stop playback - if (currentPodcast && currentPodcast.id === podcastToDelete.id) { - if (audioRef.current) { - audioRef.current.pause(); - } - setCurrentPodcast(null); - setIsPlaying(false); - } - - toast.success('Podcast deleted successfully'); - } catch (error) { - console.error('Error deleting podcast:', error); - toast.error(error instanceof Error ? error.message : 'Failed to delete podcast'); - } finally { - setIsDeleting(false); - } - }; + // Toggle mute + const toggleMute = () => { + if (audioRef.current) { + const newMutedState = !isMuted; + audioRef.current.muted = newMutedState; + setIsMuted(newMutedState); - return ( - -
-
-

Podcasts

-

Listen to generated podcasts.

-
- - {/* Filter and Search Bar */} -
-
-
- - setSearchQuery(e.target.value)} - /> -
-
- -
- -
-
- - {/* Status Messages */} - {isLoading && ( -
-
-
-

Loading podcasts...

-
-
- )} - - {error && !isLoading && ( -
-

Error loading podcasts

-

{error}

-
- )} - - {!isLoading && !error && filteredPodcasts.length === 0 && ( -
- -

No podcasts found

-

- {searchQuery - ? 'Try adjusting your search filters' - : 'Generate podcasts from your chats to get started'} -

-
- )} - - {/* Podcast Grid */} - {!isLoading && !error && filteredPodcasts.length > 0 && ( - - - {filteredPodcasts.map((podcast, index) => ( - { + if (audioRef.current) { + audioRef.current.currentTime = Math.min( + audioRef.current.duration, + audioRef.current.currentTime + 10, + ); + } + }; + + // Skip backward 10 seconds + const skipBackward = () => { + if (audioRef.current) { + audioRef.current.currentTime = Math.max( + 0, + audioRef.current.currentTime - 10, + ); + } + }; + + // Format time in MM:SS + const formatTime = (time: number) => { + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`; + }; + + // Play podcast - Fetch blob and set object URL + const playPodcast = async (podcast: PodcastItem) => { + // If the same podcast is selected, just toggle play/pause + if (currentPodcast && currentPodcast.id === podcast.id) { + togglePlayPause(); + return; + } + + // Prevent multiple simultaneous loading requests + if (isAudioLoading) { + return; + } + + try { + // Reset player state and show loading + setCurrentPodcast(podcast); + setAudioSrc(undefined); + setCurrentTime(0); + setDuration(0); + setIsPlaying(false); + setIsAudioLoading(true); + + const token = localStorage.getItem("surfsense_bearer_token"); + if (!token) { + throw new Error("Authentication token not found."); + } + + // Revoke previous object URL if exists (only after we've started the new request) + if (currentObjectUrlRef.current) { + URL.revokeObjectURL(currentObjectUrlRef.current); + currentObjectUrlRef.current = null; + } + + // Use AbortController to handle timeout or cancellation + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout + + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcast.id}/stream`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + signal: controller.signal, + }, + ); + + if (!response.ok) { + throw new Error( + `Failed to fetch audio stream: ${response.statusText}`, + ); + } + + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); + currentObjectUrlRef.current = objectUrl; + + // Set audio source + setAudioSrc(objectUrl); + + // Wait for the audio to be ready before playing + // We'll handle actual playback in the onLoadedData event instead of here + } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + throw new Error("Request timed out. Please try again."); + } + throw error; + } finally { + clearTimeout(timeoutId); + } + } catch (error) { + console.error("Error fetching or playing podcast:", error); + toast.error( + error instanceof Error + ? error.message + : "Failed to load podcast audio.", + ); + // Reset state on error + setCurrentPodcast(null); + setAudioSrc(undefined); + } finally { + setIsAudioLoading(false); + } + }; + + // Function to handle podcast deletion + const handleDeletePodcast = async () => { + if (!podcastToDelete) return; + + setIsDeleting(true); + try { + const token = localStorage.getItem("surfsense_bearer_token"); + if (!token) { + setIsDeleting(false); + return; + } + + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcastToDelete.id}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }, + ); + + if (!response.ok) { + throw new Error(`Failed to delete podcast: ${response.statusText}`); + } + + // Close dialog and refresh podcasts + setDeleteDialogOpen(false); + setPodcastToDelete(null); + + // Update local state by removing the deleted podcast + setPodcasts((prevPodcasts) => + prevPodcasts.filter((podcast) => podcast.id !== podcastToDelete.id), + ); + + // If the current playing podcast is deleted, stop playback + if (currentPodcast && currentPodcast.id === podcastToDelete.id) { + if (audioRef.current) { + audioRef.current.pause(); + } + setCurrentPodcast(null); + setIsPlaying(false); + } + + toast.success("Podcast deleted successfully"); + } catch (error) { + console.error("Error deleting podcast:", error); + toast.error( + error instanceof Error ? error.message : "Failed to delete podcast", + ); + } finally { + setIsDeleting(false); + } + }; + + return ( + +
+
+

Podcasts

+

Listen to generated podcasts.

+
+ + {/* Filter and Search Bar */} +
+
+
+ + setSearchQuery(e.target.value)} + /> +
+
+ +
+ +
+
+ + {/* Status Messages */} + {isLoading && ( +
+
+
+

+ Loading podcasts... +

+
+
+ )} + + {error && !isLoading && ( +
+

Error loading podcasts

+

{error}

+
+ )} + + {!isLoading && !error && filteredPodcasts.length === 0 && ( +
+ +

No podcasts found

+

+ {searchQuery + ? "Try adjusting your search filters" + : "Generate podcasts from your chats to get started"} +

+
+ )} + + {/* Podcast Grid */} + {!isLoading && !error && filteredPodcasts.length > 0 && ( + + + {filteredPodcasts.map((podcast, index) => ( + playPodcast(podcast)} - > -
- {/* Podcast image with gradient overlay */} - Podcast illustration - - {/* Better overlay with gradient for improved text legibility */} -
- - {/* Loading indicator with improved animation */} - {currentPodcast?.id === podcast.id && isAudioLoading && ( - - -
-

Loading podcast...

-
-
- )} + layout + onClick={() => playPodcast(podcast)} + > +
+ {/* Podcast image with gradient overlay */} + Podcast illustration - {/* Play button with animations */} - {!(currentPodcast?.id === podcast.id && (isPlaying || isAudioLoading)) && ( - -
+ + {/* Loading indicator with improved animation */} + {currentPodcast?.id === podcast.id && isAudioLoading && ( + + +
+

+ Loading podcast... +

+
+
+ )} + + {/* Play button with animations */} + {!( + currentPodcast?.id === podcast.id && + (isPlaying || isAudioLoading) + ) && ( + + - - )} - - {/* Pause button with animations */} - {currentPodcast?.id === podcast.id && isPlaying && !isAudioLoading && ( - - + + )} + + {/* Pause button with animations */} + {currentPodcast?.id === podcast.id && + isPlaying && + !isAudioLoading && ( + + - - )} - - {/* Now playing indicator */} - {currentPodcast?.id === podcast.id && !isAudioLoading && ( -
- - - - - Now Playing -
- )} -
+ onClick={(e) => { + e.stopPropagation(); + togglePlayPause(); + }} + disabled={isAudioLoading} + > + + + + +
+ )} -
-

- {podcast.title || 'Untitled Podcast'} -

-

- - {format(new Date(podcast.created_at), 'MMM d, yyyy')} -

-
- - {currentPodcast?.id === podcast.id && !isAudioLoading && ( - -
{ - e.stopPropagation(); - if (!audioRef.current || !duration) return; - const container = e.currentTarget; - const rect = container.getBoundingClientRect(); - const x = e.clientX - rect.left; - const percentage = Math.max(0, Math.min(1, x / rect.width)); - const newTime = percentage * duration; - handleSeek([newTime]); - }} - > - - + + + + + Now Playing +
+ )} +
+ +
+

+ {podcast.title || "Untitled Podcast"} +

+

+ + {format(new Date(podcast.created_at), "MMM d, yyyy")} +

+
+ + {currentPodcast?.id === podcast.id && !isAudioLoading && ( + +
{ + e.stopPropagation(); + if (!audioRef.current || !duration) return; + const container = e.currentTarget; + const rect = container.getBoundingClientRect(); + const x = e.clientX - rect.left; + const percentage = Math.max( + 0, + Math.min(1, x / rect.width), + ); + const newTime = percentage * duration; + handleSeek([newTime]); + }} + > + + - -
-
- {formatTime(currentTime)} - {formatTime(duration)} -
-
- )} + whileHover={{ scale: 1.5 }} + /> +
+
+
+ {formatTime(currentTime)} + {formatTime(duration)} +
+
+ )} - {currentPodcast?.id === podcast.id && !isAudioLoading && ( - - - - - - - - - - - - )} - -
- - - - - - { - e.stopPropagation(); - setPodcastToDelete({ id: podcast.id, title: podcast.title }); - setDeleteDialogOpen(true); - }} - > - - Delete Podcast - - - -
+ {currentPodcast?.id === podcast.id && !isAudioLoading && ( + + + + + + + + + + + + )} - - ))} - - - )} - - {/* Current Podcast Player (Fixed at bottom) */} - {currentPodcast && !isAudioLoading && audioSrc && ( - -
-
-
- - - -
- -
-

{currentPodcast.title}

- -
-
- - -
-
- {formatTime(currentTime)} / {formatTime(duration)} -
-
-
- -
- - - - - - - - - - - - -
- - - - -
- - -
-
-
-
-
-
- )} - - - {/* Delete Confirmation Dialog */} - - - - - - Delete Podcast - - - Are you sure you want to delete {podcastToDelete?.title}? This action cannot be undone. - - - - - - - - - - {/* Hidden audio element for playback */} -