"use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useState, useEffect, useRef } from "react"; import { Plus, Settings, RefreshCw } from "lucide-react"; import { cn } from "@/utils/cn"; import { api } from "@/lib/api"; import { useAuth } from "@/lib/auth-context"; import { useAudio } from "@/lib/audio-context"; import { useIsMobile, useIsTablet } from "@/hooks/useMediaQuery"; import { useToast } from "@/lib/toast-context"; import Image from "next/image"; import { MobileSidebar } from "./MobileSidebar"; const navigation = [ { name: "Library", href: "/library" }, { name: "Radio", href: "/radio" }, { name: "Discovery", href: "/discover" }, { name: "Audiobooks", href: "/audiobooks" }, { name: "Podcasts", href: "/podcasts" }, { name: "Browse", href: "/browse/playlists", badge: "Beta" }, ] as const; interface Playlist { id: string; name: string; trackCount: number; isHidden?: boolean; isOwner?: boolean; user?: { username: string }; } export function Sidebar() { const pathname = usePathname(); const { isAuthenticated } = useAuth(); const { toast } = useToast(); const { currentTrack, currentAudiobook, currentPodcast, playbackType } = useAudio(); const isMobile = useIsMobile(); const isTablet = useIsTablet(); const isMobileOrTablet = isMobile || isTablet; const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [playlists, setPlaylists] = useState([]); const [isLoadingPlaylists, setIsLoadingPlaylists] = useState(false); const [isSyncing, setIsSyncing] = useState(false); const hasLoadedPlaylists = useRef(false); // Handle library sync - no toast, notification bar handles feedback const handleSync = async () => { if (isSyncing) return; try { setIsSyncing(true); await api.scanLibrary(); // No toast - notification will appear in the activity panel window.dispatchEvent(new CustomEvent("notifications-changed")); } catch (error) { console.error("Failed to trigger library scan:", error); toast.error("Failed to start scan. Please try again."); } finally { // Keep syncing for a bit to show the animation setTimeout(() => setIsSyncing(false), 2000); } }; // Load playlists only once useEffect(() => { let loadingTimeout: NodeJS.Timeout | null = null; const loadPlaylists = async () => { if (!isAuthenticated || hasLoadedPlaylists.current) return; // Delay showing loading state to avoid flicker loadingTimeout = setTimeout(() => setIsLoadingPlaylists(true), 200); hasLoadedPlaylists.current = true; try { const data = await api.getPlaylists(); setPlaylists(data); } catch (error) { console.error("Failed to load playlists:", error); hasLoadedPlaylists.current = false; // Allow retry on error } finally { if (loadingTimeout) clearTimeout(loadingTimeout); setIsLoadingPlaylists(false); } }; loadPlaylists(); // Listen for playlist events to refresh playlists const handlePlaylistEvent = async () => { if (!isAuthenticated) return; try { const data = await api.getPlaylists(); setPlaylists(data); } catch (error) { console.error("Failed to reload playlists:", error); } }; window.addEventListener("playlist-created", handlePlaylistEvent); window.addEventListener("playlist-updated", handlePlaylistEvent); window.addEventListener("playlist-deleted", handlePlaylistEvent); return () => { if (loadingTimeout) { clearTimeout(loadingTimeout); } window.removeEventListener("playlist-created", handlePlaylistEvent); window.removeEventListener("playlist-updated", handlePlaylistEvent); window.removeEventListener("playlist-deleted", handlePlaylistEvent); }; }, [isAuthenticated]); // Close mobile menu when route changes useEffect(() => { setIsMobileMenuOpen(false); }, [pathname]); // Close mobile menu on escape key useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") setIsMobileMenuOpen(false); }; if (isMobileMenuOpen) { document.addEventListener("keydown", handleEscape); document.body.style.overflow = "hidden"; } return () => { document.removeEventListener("keydown", handleEscape); document.body.style.overflow = "unset"; }; }, [isMobileMenuOpen]); // Listen for toggle event from TopBar useEffect(() => { const handleToggle = () => setIsMobileMenuOpen(true); window.addEventListener("toggle-mobile-menu", handleToggle); return () => window.removeEventListener("toggle-mobile-menu", handleToggle); }, []); // Don't show sidebar on login/register pages // (Check after all hooks to comply with Rules of Hooks) if (pathname === "/login" || pathname === "/register") { return null; } // Render sidebar content inline to prevent component recreation const sidebarContent = ( <> {/* Mobile Only - Logo and App Info */} {isMobileOrTablet && (
{/* Logo and Title */}
Lidify

Lidify

{!currentTrack && !currentAudiobook && !currentPodcast ? (

Stream Your Way

) : (
Listening to:{" "} {playbackType === "track" && currentTrack ? `${currentTrack.artist?.name} - ${currentTrack.album?.title}` : playbackType === "audiobook" && currentAudiobook ? currentAudiobook.title : playbackType === "podcast" && currentPodcast ? currentPodcast.podcastTitle : ""}
)}
{/* Quick Actions - Settings and Sync */}
)} {/* Navigation */} {/* Playlists Section */}
Your playlists
{isLoadingPlaylists ? ( // Loading skeleton with shimmer <> {[1, 2, 3, 4, 5].map((i) => (
))} ) : playlists.filter((p) => !p.isHidden).length > 0 ? ( playlists .filter((p) => !p.isHidden) // Filter out hidden playlists .map((playlist) => { const isActive = pathname === `/playlist/${playlist.id}`; const isShared = playlist.isOwner === false; return ( {/* Hover shimmer effect */} {!isActive && (
)}
{playlist.name}
{isShared && ( )}
{isShared ? `by ${ playlist.user?.username || "Shared" }` : "Playlist"}{" "} • {playlist.trackCount} track {playlist.trackCount !== 1 ? "s" : ""}
); }) ) : (
No playlists yet
Create your first playlist to get started
)}
); return ( <> {/* Mobile Sidebar */} {isMobileOrTablet && ( setIsMobileMenuOpen(false)} /> )} {/* Desktop Sidebar */} {!isMobileOrTablet && ( )} ); }