"use client"; import { useEffect, useState, useRef, useMemo } from "react"; import { api } from "@/lib/api"; import { useRouter } from "next/navigation"; import { useAuth } from "@/lib/auth-context"; import { Loader2, Mic2, Search, Plus } from "lucide-react"; import { useToast } from "@/lib/toast-context"; import { GradientSpinner } from "@/components/ui/GradientSpinner"; import { usePodcastsQuery, useTopPodcastsQuery } from "@/hooks/useQueries"; import Image from "next/image"; // Always proxy images through the backend for caching and mobile compatibility const getProxiedImageUrl = (imageUrl: string | undefined): string | null => { if (!imageUrl) return null; return api.getCoverArtUrl(imageUrl, 300); }; interface Podcast { id: string; title: string; author: string; description?: string; coverUrl: string; autoDownloadEpisodes: boolean; genres?: string[]; feedUrl?: string; episodes?: any[]; episodeCount?: number; } interface SearchResult { type?: string; id: number; name?: string; artist?: string; title?: string; author?: string; coverUrl: string; feedUrl: string; trackCount?: number; itunesId?: number; } export default function PodcastsPage() { const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [showDropdown, setShowDropdown] = useState(false); const searchTimeoutRef = useRef(null); const dropdownRef = useRef(null); const { isAuthenticated } = useAuth(); const router = useRouter(); const { toast } = useToast(); // Use React Query hooks const { data: podcasts = [], isLoading: isLoadingPodcasts } = usePodcastsQuery(); const { data: topPodcasts = [], isLoading: isLoadingTopPodcasts } = useTopPodcastsQuery(12); // Load discovery data manually (complex multi-genre fetch) const [relatedPodcasts, setRelatedPodcasts] = useState<{ [key: string]: SearchResult[]; }>({}); // Sorting and pagination state for "My Podcasts" type SortOption = 'title' | 'author' | 'recent'; const [sortBy, setSortBy] = useState('title'); const [itemsPerPage, setItemsPerPage] = useState(50); const [currentPage, setCurrentPage] = useState(1); useEffect(() => { if (isAuthenticated) { loadDiscovery(); } }, [isAuthenticated]); const isLoading = isLoadingPodcasts || isLoadingTopPodcasts; // Sort and paginate "My Podcasts" const sortedPodcasts = useMemo(() => { const sorted = [...podcasts]; switch (sortBy) { case 'title': sorted.sort((a, b) => a.title.localeCompare(b.title)); break; case 'author': sorted.sort((a, b) => a.author.localeCompare(b.author)); break; case 'recent': // Sort by episode count (most episodes = most likely actively listened) sorted.sort((a, b) => (b.episodeCount || 0) - (a.episodeCount || 0)); break; } return sorted; }, [podcasts, sortBy]); const totalPages = Math.ceil(sortedPodcasts.length / itemsPerPage); const paginatedPodcasts = useMemo(() => { const start = (currentPage - 1) * itemsPerPage; return sortedPodcasts.slice(start, start + itemsPerPage); }, [sortedPodcasts, currentPage, itemsPerPage]); // Reset page when sort changes useEffect(() => { setCurrentPage(1); }, [sortBy]); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) ) { setShowDropdown(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); // Debounced search useEffect(() => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } if (searchQuery.trim().length < 2) { setSearchResults([]); setShowDropdown(false); return; } setIsSearching(true); searchTimeoutRef.current = setTimeout(async () => { try { // Use discover endpoint to search iTunes for NEW podcasts const results = await api.discoverSearch( searchQuery, "podcasts", 8 ); // Filter for podcasts from the results array const podcastResults = results?.results?.filter( (r: any) => r.type === "podcast" ) || []; setSearchResults(podcastResults); setShowDropdown(podcastResults.length > 0); } catch (error) { console.error("Podcast search failed:", error); setSearchResults([]); setShowDropdown(false); } finally { setIsSearching(false); } }, 500); return () => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } }; }, [searchQuery]); const loadDiscovery = async () => { try { // Load popular genres // iTunes genre IDs: Comedy=1303, Society&Culture=1324, News=1489, // True Crime=1488, Business=1321, Sports=1545, Leisure=1502 const genreIds = [ 1303, // Comedy 1324, // Society & Culture 1489, // News 1488, // True Crime 1321, // Business 1545, // Sports 1502, // Leisure (Gaming & Hobbies) ]; const genreData = await api.getPodcastsByGenre(genreIds); setRelatedPodcasts(genreData); } catch (error) { console.error("Failed to load podcast discovery:", error); } }; const handleSubscribe = async (result: SearchResult | any) => { try { toast.info(`Subscribing to ${result.name || result.title}...`); // For top/genre podcasts from RSS, we have itunesId but no feedUrl // Pass itunesId and let backend look up the feedUrl const itunesId = result.itunesId?.toString() || result.id?.toString(); const response = await api.subscribePodcast( result.feedUrl || "", itunesId ); if (response.success && response.podcast?.id) { toast.success(`Subscribed to ${result.name || result.title}!`); setSearchQuery(""); setShowDropdown(false); // Navigate to new podcast (React Query will automatically refetch) router.push(`/podcasts/${response.podcast.id}`); } } catch (error: any) { console.error("Subscribe error:", error); toast.error(error.message || "Failed to subscribe"); } }; if (isLoading) { return (
); } return (
{/* Quick gradient fade - yellow to purple */}
{/* Hero Section */}

Podcasts

{/* Quick Search - Full Width on Mobile */}
setSearchQuery(e.target.value)} placeholder="Quick add..." className="w-full pl-11 pr-4 py-3 bg-white/5 border border-white/10 rounded-full text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 focus:bg-white/10 transition-all text-sm" /> {isSearching && (
)} {/* Dropdown Results */} {showDropdown && searchResults.length > 0 && (
{searchResults.map((result) => { const imageUrl = getProxiedImageUrl(result.coverUrl); return (
{ router.push( `/podcasts/${result.id}` ); setShowDropdown(false); }} > {/* Cover Art */}
{imageUrl ? ( {result.name ) : (
)}
{/* Info */}

{result.name}

{result.artist}

{/* Add Button */}
); })}
)} {/* No Results */} {showDropdown && searchResults.length === 0 && !isSearching && searchQuery.length >= 2 && (

No podcasts found for "{searchQuery}"

)}
{/* My Podcasts */} {podcasts.length > 0 && (

My Podcasts

{/* Sort Dropdown */} {/* Items per page */} {podcasts.length} {podcasts.length === 1 ? 'podcast' : 'podcasts'}
{paginatedPodcasts.map((podcast, index) => { const imageUrl = getProxiedImageUrl(podcast.coverUrl); return (
router.push(`/podcasts/${podcast.id}`) } data-tv-card data-tv-card-index={index} tabIndex={0} className="bg-transparent hover:bg-white/5 transition-all p-3 rounded-md cursor-pointer group" >
{imageUrl ? ( {podcast.title} ) : (
)}

{podcast.title}

{podcast.author}

); })}
{/* Pagination Controls */} {totalPages > 1 && (
Page {currentPage} of {totalPages}
)}
)} {/* Top Podcasts */} {topPodcasts.length > 0 && (

Top Podcasts

{topPodcasts.map((podcast, index) => { const imageUrl = getProxiedImageUrl(podcast.coverUrl); return (
router.push(`/podcasts/${podcast.id}`) } data-tv-card data-tv-card-index={index} tabIndex={0} className="bg-transparent hover:bg-white/5 transition-all p-3 rounded-md cursor-pointer group" >
{imageUrl ? ( {podcast.title} ) : (
)}

{podcast.title}

{podcast.author}

); })}
)} {/* Genre-based Discovery - Ordered by popularity */} {[ { id: "1303", name: "Comedy" }, { id: "1324", name: "Society & Culture" }, { id: "1489", name: "News" }, { id: "1488", name: "True Crime" }, { id: "1321", name: "Business" }, { id: "1545", name: "Sports" }, { id: "1502", name: "Leisure" }, ].map(({ id: genreId, name: genreName }) => { const genrePodcasts = relatedPodcasts[genreId] || []; return genrePodcasts.length > 0 ? (

{genreName}

{genrePodcasts.map((podcast, index) => { const imageUrl = getProxiedImageUrl(podcast.coverUrl); return (
router.push( `/podcasts/${podcast.id}` ) } data-tv-card data-tv-card-index={index} tabIndex={0} className="bg-transparent hover:bg-white/5 transition-all p-3 rounded-md cursor-pointer group" >
{imageUrl ? ( {podcast.title} ) : (
)}

{podcast.title}

{podcast.author}

); })}
) : null; })} {/* Empty State */} {podcasts.length === 0 && topPodcasts.length === 0 && (

Discover Podcasts

Search for podcasts above to subscribe and start listening

)}
); }