"use client"; import { useState, useEffect, useRef, useMemo } from "react"; import { Radio, Play, Loader2, Shuffle, ChevronLeft, ChevronRight, } from "lucide-react"; import { api } from "@/lib/api"; import { useAudioControls } from "@/lib/audio-controls-context"; import { Track } from "@/lib/audio-state-context"; import { toast } from "sonner"; import { cn } from "@/utils/cn"; import { useIsMobile, useIsTablet } from "@/hooks/useMediaQuery"; interface RadioStation { id: string; name: string; description: string; color: string; filter: { type: | "genre" | "decade" | "discovery" | "favorites" | "all" | "workout"; value?: string; }; minTracks?: number; } interface GenreCount { genre: string; count: number; } // Static radio stations (always shown if tracks exist) const STATIC_STATIONS: RadioStation[] = [ { id: "all", name: "Shuffle All", description: "Your entire library", color: "from-[#ecb200]/60 to-amber-600/40", filter: { type: "all" }, minTracks: 10, }, { id: "workout", name: "Workout", description: "High energy tracks", color: "from-red-500/50 to-orange-600/40", filter: { type: "workout" }, minTracks: 15, }, { id: "discovery", name: "Discovery", description: "Lesser-played gems", color: "from-emerald-500/50 to-teal-600/40", filter: { type: "discovery" }, minTracks: 20, }, { id: "favorites", name: "Favorites", description: "Most played", color: "from-rose-500/50 to-pink-600/40", filter: { type: "favorites" }, minTracks: 10, }, ]; interface DecadeCount { decade: number; count: number; } // Decade color mapping const DECADE_COLORS: Record = { 1700: "from-amber-800/50 to-yellow-900/40", 1800: "from-slate-500/50 to-gray-600/40", 1900: "from-amber-400/50 to-yellow-500/40", 1920: "from-yellow-500/50 to-amber-600/40", 1940: "from-red-400/50 to-orange-500/40", 1950: "from-pink-400/50 to-red-500/40", 1960: "from-amber-500/50 to-orange-600/40", 1970: "from-orange-500/50 to-red-600/40", 1980: "from-fuchsia-500/50 to-purple-600/40", 1990: "from-purple-500/50 to-violet-600/40", 2000: "from-blue-500/50 to-cyan-600/40", 2010: "from-teal-500/50 to-emerald-600/40", 2020: "from-orange-500/50 to-amber-600/40", }; const getDecadeColor = (decade: number): string => { const knownDecades = Object.keys(DECADE_COLORS) .map(Number) .sort((a, b) => b - a); for (const known of knownDecades) { if (decade >= known) { return DECADE_COLORS[known]; } } return "from-gray-500/50 to-slate-600/40"; }; const getDecadeName = (decade: number): string => { if (decade < 1900) return `${decade}s`; if (decade < 2000) return `${decade.toString().slice(2)}s`; return `${decade}s`; }; // Genre color mapping const GENRE_COLORS: Record = { rock: "from-red-500/50 to-orange-600/40", pop: "from-pink-500/50 to-rose-600/40", "hip hop": "from-purple-500/50 to-indigo-600/40", "hip-hop": "from-purple-500/50 to-indigo-600/40", rap: "from-purple-500/50 to-indigo-600/40", electronic: "from-cyan-500/50 to-blue-600/40", jazz: "from-amber-500/50 to-yellow-600/40", classical: "from-slate-400/50 to-gray-500/40", metal: "from-zinc-600/50 to-neutral-700/40", country: "from-orange-400/50 to-amber-500/40", folk: "from-green-500/50 to-emerald-600/40", indie: "from-violet-500/50 to-purple-600/40", alternative: "from-indigo-500/50 to-blue-600/40", "r&b": "from-fuchsia-500/50 to-pink-600/40", soul: "from-amber-600/50 to-orange-700/40", blues: "from-blue-600/50 to-indigo-700/40", punk: "from-lime-500/50 to-green-600/40", reggae: "from-green-400/50 to-yellow-500/40", default: "from-gray-500/50 to-slate-600/40", }; const getGenreColor = (genre: string): string => { const lower = genre.toLowerCase(); return GENRE_COLORS[lower] || GENRE_COLORS.default; }; export function LibraryRadioStations() { const { playTracks } = useAudioControls(); const [loadingStation, setLoadingStation] = useState(null); const [genres, setGenres] = useState([]); const [decades, setDecades] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const fetchData = async () => { try { const [genresRes, decadesRes] = await Promise.all([ api.get<{ genres: GenreCount[] }>("/library/genres"), api.get<{ decades: DecadeCount[] }>("/library/decades"), ]); const validGenres = (genresRes.genres || []) .filter((g) => g.count >= 15) .slice(0, 6); setGenres(validGenres); setDecades((decadesRes.decades || []).slice(0, 4)); } catch (error) { console.error("Failed to fetch radio data:", error); } finally { setIsLoading(false); } }; fetchData(); }, []); const startRadio = async (station: RadioStation) => { setLoadingStation(station.id); try { const params = new URLSearchParams(); params.set("type", station.filter.type); if (station.filter.value) { params.set("value", station.filter.value); } params.set("limit", "100"); const response = await api.get<{ tracks: Track[] }>( `/library/radio?${params.toString()}` ); if (!response.tracks || response.tracks.length === 0) { toast.error(`No tracks found for ${station.name}`); return; } if (response.tracks.length < (station.minTracks || 10)) { toast.error(`Not enough tracks for ${station.name} radio`, { description: `Found ${ response.tracks.length }, need at least ${station.minTracks || 10}`, }); return; } const shuffled = [...response.tracks].sort( () => Math.random() - 0.5 ); playTracks(shuffled, 0); toast.success(`${station.name} Radio`, { description: `Shuffling ${shuffled.length} tracks`, icon: , }); } catch (error) { console.error("Failed to start radio:", error); toast.error("Failed to start radio station"); } finally { setLoadingStation(null); } }; const allStations = useMemo(() => { const genreStations: RadioStation[] = genres.map((g) => ({ id: `genre-${g.genre}`, name: g.genre, description: `${g.count} tracks`, color: getGenreColor(g.genre), filter: { type: "genre" as const, value: g.genre }, minTracks: 15, })); const decadeStations: RadioStation[] = decades.map((d) => ({ id: `decade-${d.decade}`, name: getDecadeName(d.decade), description: `${d.count} tracks`, color: getDecadeColor(d.decade), filter: { type: "decade" as const, value: d.decade.toString() }, minTracks: 15, })); return [...STATIC_STATIONS, ...genreStations, ...decadeStations]; }, [genres, decades]); const scrollRef = useRef(null); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(false); const [currentPage, setCurrentPage] = useState(0); const isMobile = useIsMobile(); const isTablet = useIsTablet(); const isMobileOrTablet = isMobile || isTablet; // Group stations into pages of 6 (2x3 grid) for mobile only const stationPages = useMemo(() => { const pages: RadioStation[][] = []; for (let i = 0; i < allStations.length; i += 6) { pages.push(allStations.slice(i, i + 6)); } return pages; }, [allStations]); const checkScroll = () => { const el = scrollRef.current; if (!el) return; setCanScrollLeft(el.scrollLeft > 0); setCanScrollRight(el.scrollLeft < el.scrollWidth - el.clientWidth - 1); // Update current page for mobile if (isMobileOrTablet) { const pageWidth = el.clientWidth; const newPage = Math.round(el.scrollLeft / pageWidth); setCurrentPage(newPage); } }; useEffect(() => { checkScroll(); const el = scrollRef.current; if (el) { el.addEventListener("scroll", checkScroll); window.addEventListener("resize", checkScroll); } return () => { if (el) el.removeEventListener("scroll", checkScroll); window.removeEventListener("resize", checkScroll); }; }, [stationPages, isMobileOrTablet]); const scroll = (direction: "left" | "right") => { const el = scrollRef.current; if (!el) return; const scrollAmount = isMobileOrTablet ? el.clientWidth : el.clientWidth * 0.8; el.scrollBy({ left: direction === "left" ? -scrollAmount : scrollAmount, behavior: "smooth", }); }; // Desktop: Compact horizontal card const renderDesktopCard = (station: RadioStation) => ( ); // Mobile: Card for 2x3 grid const renderMobileCard = (station: RadioStation) => ( ); // Desktop layout: single row horizontal carousel if (!isMobileOrTablet) { return (
{canScrollLeft && ( )}
{allStations.map((station) => renderDesktopCard(station))} {isLoading && Array.from({ length: 6 }).map((_, i) => (
))}
{canScrollRight && ( )}
); } // Mobile/Tablet layout: 2x3 grid pages return (
{stationPages.map((page, pageIndex) => (
{page.map((station) => renderMobileCard(station))} {page.length < 6 && Array.from({ length: 6 - page.length }).map( (_, i) => (
) )}
))} {isLoading && (
{Array.from({ length: 6 }).map((_, i) => (
))}
)}
{stationPages.length > 1 && (
{stationPages.map((_, index) => (
)}
); }