"use client"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { useState, useEffect, useRef } from "react"; import { Home, Search, Settings, RefreshCw, Power, Menu, Bell } from "lucide-react"; import { ActivityPanelToggle } from "./ActivityPanel"; import { cn } from "@/utils/cn"; import { api } from "@/lib/api"; import { useToast } from "@/lib/toast-context"; import { useJobStatus } from "@/hooks/useJobStatus"; import { useDownloadContext } from "@/lib/download-context"; import { useIsMobile, useIsTablet } from "@/hooks/useMediaQuery"; import { useAuth } from "@/lib/auth-context"; import { useQueryClient } from "@tanstack/react-query"; import Image from "next/image"; export function TopBar() { const pathname = usePathname(); const router = useRouter(); const { logout } = useAuth(); const isMobile = useIsMobile(); const isTablet = useIsTablet(); const isMobileOrTablet = isMobile || isTablet; const [searchQuery, setSearchQuery] = useState(""); const [scanJobId, setScanJobId] = useState(null); const [lastScanTime, setLastScanTime] = useState(0); const { toast } = useToast(); const searchTimeoutRef = useRef(null); const queryClient = useQueryClient(); const { jobStatus, isPolling } = useJobStatus(scanJobId, "scan", { onComplete: () => { // Refresh Activity Panel and enrichment progress after scan queryClient.invalidateQueries({ queryKey: ["notifications"] }); queryClient.invalidateQueries({ queryKey: ["enrichment-progress"] }); setScanJobId(null); }, onError: () => { // Scan errors will show in the activity panel via notifications setScanJobId(null); }, }); // Track download status from context (single source of truth) const { pendingDownloads, downloadStatus } = useDownloadContext(); // Only use API-driven state for the icon // pendingDownloads is optimistic local state that can become stale const hasActiveDownloads = downloadStatus.hasActiveDownloads; const hasPendingUploads = pendingDownloads.length > 0 && pendingDownloads.some(p => Date.now() - p.timestamp < 30000); // Only count recent pending const hasFailedDownloads = downloadStatus.failedDownloads.length > 0; const handleSync = async () => { if (isPolling) return; // Prevent spam clicking - cooldown of 5 seconds (silently ignore) const now = Date.now(); const timeSinceLastScan = now - lastScanTime; if (timeSinceLastScan < 5000) { return; } try { setLastScanTime(now); const response = await api.scanLibrary(); setScanJobId(response.jobId); // Refresh notifications to show the scan started notification queryClient.invalidateQueries({ queryKey: ["notifications"] }); } catch (error) { console.error("Failed to trigger library scan:", error); // Scan errors will show in the activity panel via notifications } }; const handleLogout = async () => { try { await logout(); toast.success("Logged out successfully"); } catch (error) { console.error("Logout error:", error); toast.error("Failed to logout"); } }; const handleSearch = (e: React.FormEvent) => { e.preventDefault(); if (searchQuery.trim()) { router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`); } }; // Auto-search with debounce (500ms after user stops typing) useEffect(() => { // Don't auto-search if we're already on the search page with the same query const params = new URLSearchParams(window.location.search); const currentQuery = params.get("q"); if (pathname === "/search" && currentQuery === searchQuery.trim()) { return; } // Clear any existing timeout if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } // Don't search if query is empty if (!searchQuery.trim()) { return; } // Set new timeout to trigger search after 500ms of no typing searchTimeoutRef.current = setTimeout(() => { router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`); }, 500); // Cleanup timeout on unmount or when searchQuery changes return () => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } }; }, [searchQuery, router, pathname]); // Sync search query with URL on page change useEffect(() => { const params = new URLSearchParams(window.location.search); const q = params.get("q"); if (pathname === "/search" && q) { // Only update if different to avoid loops if (q !== searchQuery) { setSearchQuery(q); } } else if (pathname !== "/search" && searchQuery) { // Clear search when leaving search page setSearchQuery(""); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathname]); // Only re-run when pathname changes return (
{/* Mobile/Tablet Layout: Hamburger + Home + Search + Bell */} {isMobileOrTablet ? ( <> {/* Hamburger menu button */} {/* Home */} {/* Search */}
setSearchQuery(e.target.value) } placeholder="Search..." aria-label="Search" autoCapitalize="none" autoCorrect="off" tabIndex={0} className="w-full h-10 pl-10 pr-3 bg-[#1a1a1a] hover:bg-[#242424] border-2 border-transparent focus:border-white/20 rounded-full text-sm text-white placeholder-gray-400 transition-all outline-none" />
{/* Notification Bell */} ) : ( <> {/* Desktop Layout */} {/* Logo - Far Left */}
Lidify Lidify
{/* Center - Home & Search */}
setSearchQuery(e.target.value) } placeholder="What do you want to play?" aria-label="Search" autoCapitalize="none" autoCorrect="off" tabIndex={0} className="w-full h-12 pl-12 pr-4 bg-[#1a1a1a] hover:bg-[#242424] border-2 border-transparent focus:border-white/20 rounded-full text-sm text-white placeholder-gray-400 transition-all outline-none" />
{/* Right - Sync & Settings */}
)}
); }