"use client"; import { useState, useEffect } from "react"; import { Radio, Play, Loader2, Shuffle, ChevronLeft } from "lucide-react"; import Link from "next/link"; import { api } from "@/lib/api"; import { useAudioControls } from "@/lib/audio-controls-context"; import { Track } from "@/lib/audio-state-context"; import { toast } from "sonner"; 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 const STATIC_STATIONS: RadioStation[] = [ { id: "all", name: "Shuffle All", description: "Your entire library", color: "from-brand/40 to-amber-600/30", filter: { type: "all" }, minTracks: 10, }, { id: "workout", name: "Workout", description: "High energy tracks", color: "from-red-500/30 to-orange-600/30", filter: { type: "workout" }, minTracks: 15, }, { id: "discovery", name: "Discovery", description: "Lesser-played gems", color: "from-emerald-500/30 to-teal-600/30", filter: { type: "discovery" }, minTracks: 20, }, { id: "favorites", name: "Favorites", description: "Most played", color: "from-rose-500/30 to-pink-600/30", filter: { type: "favorites" }, minTracks: 10, }, ]; interface DecadeCount { decade: number; count: number; } // Decade color mapping - covers from 1700s (classical) to 2020s const DECADE_COLORS: Record = { 1700: "from-amber-800/30 to-yellow-900/30", 1710: "from-amber-700/30 to-yellow-800/30", 1720: "from-amber-700/30 to-yellow-800/30", 1730: "from-amber-700/30 to-yellow-800/30", 1740: "from-amber-700/30 to-yellow-800/30", 1750: "from-amber-600/30 to-yellow-700/30", 1760: "from-amber-600/30 to-yellow-700/30", 1770: "from-amber-600/30 to-yellow-700/30", 1780: "from-amber-600/30 to-yellow-700/30", 1790: "from-amber-600/30 to-yellow-700/30", 1800: "from-slate-600/30 to-gray-700/30", 1810: "from-slate-600/30 to-gray-700/30", 1820: "from-slate-500/30 to-gray-600/30", 1830: "from-slate-500/30 to-gray-600/30", 1840: "from-slate-500/30 to-gray-600/30", 1850: "from-slate-400/30 to-gray-500/30", 1860: "from-slate-400/30 to-gray-500/30", 1870: "from-slate-400/30 to-gray-500/30", 1880: "from-slate-400/30 to-gray-500/30", 1890: "from-slate-400/30 to-gray-500/30", 1900: "from-sepia-400/30 to-amber-500/30", 1910: "from-amber-400/30 to-yellow-500/30", 1920: "from-yellow-500/30 to-amber-600/30", 1930: "from-orange-400/30 to-amber-500/30", 1940: "from-red-400/30 to-orange-500/30", 1950: "from-pink-400/30 to-red-500/30", 1960: "from-amber-500/30 to-orange-600/30", 1970: "from-orange-500/30 to-red-600/30", 1980: "from-fuchsia-500/30 to-purple-600/30", 1990: "from-purple-500/30 to-violet-600/30", 2000: "from-blue-500/30 to-cyan-600/30", 2010: "from-teal-500/30 to-emerald-600/30", 2020: "from-orange-500/30 to-amber-600/30", }; const getDecadeColor = (decade: number): string => { return DECADE_COLORS[decade] || "from-gray-500/30 to-slate-600/30"; }; const getDecadeName = (decade: number): string => { if (decade < 1900) return `${decade}s`; if (decade < 2000) return `${decade.toString().slice(2)}s`; return `${decade}s`; }; const getDecadeDescription = (decade: number, count: number): string => { return `${decade}-${decade + 9} • ${count} tracks`; }; // Genre color mapping const GENRE_COLORS: Record = { rock: "from-red-500/30 to-orange-600/30", pop: "from-pink-500/30 to-rose-600/30", "hip hop": "from-purple-500/30 to-indigo-600/30", "hip-hop": "from-purple-500/30 to-indigo-600/30", rap: "from-purple-500/30 to-indigo-600/30", electronic: "from-cyan-500/30 to-blue-600/30", jazz: "from-amber-500/30 to-yellow-600/30", classical: "from-slate-400/30 to-gray-500/30", metal: "from-zinc-600/30 to-neutral-700/30", country: "from-orange-400/30 to-amber-500/30", folk: "from-green-500/30 to-emerald-600/30", indie: "from-violet-500/30 to-purple-600/30", alternative: "from-indigo-500/30 to-blue-600/30", "r&b": "from-fuchsia-500/30 to-pink-600/30", soul: "from-amber-600/30 to-orange-700/30", blues: "from-blue-600/30 to-indigo-700/30", punk: "from-lime-500/30 to-green-600/30", reggae: "from-green-400/30 to-yellow-500/30", default: "from-gray-500/30 to-slate-600/30", }; const getGenreColor = (genre: string): string => { const lower = genre.toLowerCase(); return GENRE_COLORS[lower] || GENRE_COLORS.default; }; // Radio Station Card Component function RadioStationCard({ station, onPlay, isLoading }: { station: RadioStation; onPlay: () => void; isLoading: boolean; }) { return ( ); } // Section Header Component function SectionHeader({ title, description }: { title: string; description?: string }) { return (

{title}

{description &&

{description}

}
); } export default function RadioPage() { const { playTracks } = useAudioControls(); const [loadingStation, setLoadingStation] = useState(null); const [genres, setGenres] = useState([]); const [decades, setDecades] = useState([]); const [isLoading, setIsLoading] = useState(true); // Fetch available genres and decades from library useEffect(() => { const fetchData = async () => { try { const [genresRes, decadesRes] = await Promise.all([ api.get<{ genres: GenreCount[] }>("/library/genres"), api.get<{ decades: DecadeCount[] }>("/library/decades"), ]); // Filter to genres with enough tracks (at least 15) const validGenres = (genresRes.genres || []).filter((g) => g.count >= 15); setGenres(validGenres); // Decades already filtered by backend (15+ tracks) setDecades(decadesRes.decades || []); } 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; } // Shuffle the tracks const shuffled = [...response.tracks].sort(() => Math.random() - 0.5); // Start playing 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); } }; // Create genre stations from library 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, })); // Create decade stations from library (dynamically based on what's available) const decadeStations: RadioStation[] = decades.map((d) => ({ id: `decade-${d.decade}`, name: getDecadeName(d.decade), description: getDecadeDescription(d.decade, d.count), color: getDecadeColor(d.decade), filter: { type: "decade" as const, value: d.decade.toString() }, minTracks: 15, })); return (
{/* Hero gradient */}
{/* Content */}
{/* Back link */} Back to Home {/* Header */}

Radio Stations

Continuous shuffle from your library

{/* Quick Start Section */}
{STATIC_STATIONS.map((station) => ( startRadio(station)} isLoading={loadingStation === station.id} /> ))}
{/* Genres Section */} {(isLoading || genreStations.length > 0) && (
{isLoading ? (
{Array.from({ length: 6 }).map((_, i) => (
))}
) : (
{genreStations.map((station) => ( startRadio(station)} isLoading={loadingStation === station.id} /> ))}
)}
)} {/* Decades Section - Only show if there are decade stations */} {(isLoading || decadeStations.length > 0) && (
{isLoading ? (
{Array.from({ length: 6 }).map((_, i) => (
))}
) : (
{decadeStations.map((station) => ( startRadio(station)} isLoading={loadingStation === station.id} /> ))}
)}
)} {/* Info */}

About Radio Stations

Radio stations are generated from your personal music library. As you add more music, new genre and decade stations will automatically appear. Each station requires a minimum number of tracks to ensure a good listening experience.

); }