"use client"; import { useState, useMemo } from "react"; import { useParams, useRouter } from "next/navigation"; import Image from "next/image"; import { api } from "@/lib/api"; import { useAudio } from "@/lib/audio-context"; import { GradientSpinner } from "@/components/ui/GradientSpinner"; import { Play, Pause, Music, Shuffle, Save, ListPlus } from "lucide-react"; import { cn } from "@/utils/cn"; import { toast } from "sonner"; import { useMixQuery } from "@/hooks/useQueries"; interface MixTrack { id: string; title: string; duration: number; albumId: string; album: { title: string; coverUrl?: string; artist: { id: string; name: string; }; }; } export default function MixPage() { const params = useParams(); const router = useRouter(); const mixId = params.id as string; const { playTracks, addToQueue, currentTrack, isPlaying, pause, resume } = useAudio(); const { data: mix, isLoading } = useMixQuery(mixId); const [isSaving, setIsSaving] = useState(false); // Calculate total duration const totalDuration = useMemo(() => { if (!mix?.tracks) return 0; return mix.tracks.reduce((sum: number, track: MixTrack) => sum + (track.duration || 0), 0); }, [mix?.tracks]); const formatTotalDuration = (seconds: number) => { const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); if (hours > 0) { return `about ${hours} hr ${mins} min`; } return `${mins} min`; }; const formatDuration = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, "0")}`; }; // Check if this mix is currently playing const mixTrackIds = useMemo(() => { return new Set(mix?.tracks?.map((track: MixTrack) => track.id) || []); }, [mix?.tracks]); const isThisMixPlaying = useMemo(() => { if (!isPlaying || !currentTrack || !mix?.tracks?.length) return false; return mixTrackIds.has(currentTrack.id); }, [isPlaying, currentTrack, mixTrackIds, mix?.tracks?.length]); const formatTracksForPlayback = (tracks: MixTrack[]) => { return tracks.map((track) => ({ id: track.id, title: track.title, artist: { name: track.album.artist.name, id: track.album.artist.id, }, album: { title: track.album.title, coverArt: track.album.coverUrl, id: track.albumId, }, duration: track.duration, })); }; const handlePlayMix = () => { if (!mix?.tracks || mix.tracks.length === 0) return; // If this mix is playing, toggle pause/resume if (isThisMixPlaying) { if (isPlaying) { pause(); } else { resume(); } return; } const tracks = formatTracksForPlayback(mix.tracks); playTracks(tracks, 0); }; const handlePlayTrack = (index: number) => { if (!mix?.tracks || mix.tracks.length === 0) return; const tracks = formatTracksForPlayback(mix.tracks); playTracks(tracks, index); }; const handleShuffle = () => { if (!mix?.tracks) return; const tracks = formatTracksForPlayback(mix.tracks); const shuffled = [...tracks].sort(() => Math.random() - 0.5); playTracks(shuffled, 0); }; const handleAddToQueue = (track: MixTrack) => { const formattedTrack = { id: track.id, title: track.title, artist: { name: track.album.artist.name, id: track.album.artist.id, }, album: { title: track.album.title, coverArt: track.album.coverUrl, id: track.albumId, }, duration: track.duration, }; addToQueue(formattedTrack); toast.success(`Added ${track.title} to queue`); }; const handleSaveAsPlaylist = async () => { if (!mix) return; setIsSaving(true); try { const result = await api.saveMixAsPlaylist(mixId); toast.success(`Saved as "${result.name}" playlist!`); window.dispatchEvent(new Event("playlist-created")); setTimeout(() => { router.push(`/playlist/${result.id}`); }, 1000); } catch (error: unknown) { console.error("Failed to save mix as playlist:", error); const err = error as { status?: number; data?: { playlistId?: string } }; if (err?.status === 409) { toast.info("You've already saved this mix as a playlist."); if (err?.data?.playlistId) { setTimeout(() => { router.push(`/playlist/${err.data!.playlistId}`); }, 1000); } } else if (error instanceof Error) { toast.error(error.message); } else { toast.error("Failed to save mix as playlist"); } } finally { setIsSaving(false); } }; if (isLoading) { return (
); } if (!mix) { return (

Mix not found

); } return (
{/* Compact Hero - Spotify Style */}
{/* Cover Art Mosaic */}
{mix.coverUrls && mix.coverUrls.length > 0 ? (
{mix.coverUrls.slice(0, 4).map((url: string, index: number) => { const proxiedUrl = api.getCoverArtUrl(url, 200); return (
); })} {Array.from({ length: Math.max(0, 4 - (mix.coverUrls?.length || 0)), }).map((_, index) => (
))}
) : (
)}
{/* Mix Info - Bottom Aligned */}

Mix

{mix.name}

{mix.description && (

{mix.description}

)}
{mix.trackCount || mix.tracks?.length || 0} songs {totalDuration > 0 && ( , {formatTotalDuration(totalDuration)} )}
{/* Action Bar */}
{/* Play Button */} {mix.tracks && mix.tracks.length > 0 && ( )} {/* Shuffle Button */} {mix.tracks && mix.tracks.length > 1 && ( )} {/* Save as Playlist Button */}
{/* Track Listing */}
{mix.tracks && mix.tracks.length > 0 ? (
{/* Table Header */}
# Title Album Duration
{/* Track Rows */}
{mix.tracks.map((track: MixTrack, index: number) => { const isCurrentlyPlaying = currentTrack?.id === track.id; return (
handlePlayTrack(index)} className={cn( "grid grid-cols-[40px_1fr_auto] md:grid-cols-[40px_minmax(200px,4fr)_minmax(100px,1fr)_80px] gap-4 px-4 py-2 rounded-md hover:bg-white/5 transition-colors group cursor-pointer", isCurrentlyPlaying && "bg-white/10" )} > {/* Track Number / Play Icon */}
{isCurrentlyPlaying && isPlaying ? ( ) : ( index + 1 )}
{/* Title + Artist */}
{track.album?.coverUrl ? ( {track.title} ) : (
)}

{track.title}

{track.album.artist.name}

{/* Album (hidden on mobile) */}

{track.album.title}

{/* Duration + Actions */}
{formatDuration(track.duration)}
); })}
) : (

No tracks

This mix is empty

)}
); }