Anish-530
Fix: 404 page loader stuck, navbar loading spinners, bulletproof NR import
942a467
import React, { useState, useRef, useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
import { Menu, X, LogOut, Camera, Upload, Trash2, Moon, Sun, MessageSquare, UserCircle, LayoutDashboard, Volume2, VolumeX } from 'lucide-react';
import { apiLayer } from '@/lib/api';
import FeedbackModal from '@/components/shared/FeedbackModal';
interface NavbarProps {
onAnalyzeClick?: () => void;
}
export default function Navbar({ onAnalyzeClick }: NavbarProps) {
const router = useRouter();
const pathname = usePathname();
const { isAuthenticated, user, logout, updateUser } = useAuth();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [dropdownPinned, setDropdownPinned] = useState(false);
const [soundEnabled, setSoundEnabled] = useState(true);
const [navLoading, setNavLoading] = useState(false);
const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleMouseEnter = () => {
if (!dropdownPinned) {
if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
setDropdownOpen(true);
}
};
const handleMouseLeave = () => {
if (!dropdownPinned) {
closeTimeoutRef.current = setTimeout(() => {
setDropdownOpen(false);
}, 300); // 300ms grace period
}
};
useEffect(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('spotix_sound');
if (saved !== null) {
setSoundEnabled(saved === 'true');
}
}
}, []);
const toggleSound = (e: React.MouseEvent) => {
e.stopPropagation();
const newState = !soundEnabled;
setSoundEnabled(newState);
localStorage.setItem('spotix_sound', String(newState));
};
// Avatar logic
const fileInputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const [localAvatar, setLocalAvatar] = useState<string | null>(null);
// UI State
const [feedbackModalOpen, setFeedbackModalOpen] = useState(false);
useEffect(() => {
if (user?.avatar_url) {
setLocalAvatar(user.avatar_url);
} else if (user?.google_avatar_url) {
setLocalAvatar(user.google_avatar_url);
} else {
setLocalAvatar(null);
}
}, [user?.avatar_url, user?.google_avatar_url]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setDropdownOpen(false);
setDropdownPinned(false);
}
};
const handleEsc = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setDropdownOpen(false);
setDropdownPinned(false);
}
};
if (dropdownOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEsc);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEsc);
};
}, [dropdownOpen]);
const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
alert("Only image files are allowed for avatars.");
return;
}
const reader = new FileReader();
reader.onloadend = async () => {
const base64String = reader.result as string;
setLocalAvatar(base64String);
try {
await apiLayer.updateAvatar(base64String);
updateUser({ avatar_url: base64String });
} catch (err) {
console.error("Failed to update avatar:", err);
}
};
reader.readAsDataURL(file);
};
const handleRemoveAvatar = async () => {
if (user?.google_avatar_url) {
setLocalAvatar(user.google_avatar_url);
} else {
setLocalAvatar(null);
}
try {
await apiLayer.updateAvatar("");
updateUser({ avatar_url: null });
} catch (err) {
console.error("Failed to remove avatar:", err);
}
setDropdownOpen(false);
};
return (
<>
<nav className="fixed top-6 left-1/2 -translate-x-1/2 z-[60] w-[92%] max-w-5xl">
<div className="bg-[var(--theme-bg)]/60 backdrop-blur-xl border border-[#4d453e]/15 rounded-full px-6 md:px-8 py-3 md:py-4 flex justify-between items-center shadow-[0_20px_50px_rgba(0,0,0,0.4)] transition-all duration-500">
<div onClick={() => router.push('/')} className="cursor-pointer text-base md:text-lg font-black tracking-tighter text-[var(--theme-text)] uppercase hover:opacity-80 transition-opacity">SPOTIX</div>
<div className="hidden lg:flex items-center gap-10">
<a className={`font-['Inter'] tracking-tight font-medium text-[11px] uppercase transition-colors nav-link ${pathname === '/' ? 'text-[var(--theme-text)]' : 'text-[#d0c4bb] hover:text-[var(--theme-text)]'}`} href="/">Platform</a>
{isAuthenticated && (
<a onClick={() => router.push('/dashboard')} className={`cursor-pointer font-['Inter'] tracking-tight font-medium text-[11px] uppercase transition-colors nav-link ${pathname === '/dashboard' ? 'text-[var(--theme-text)]' : 'text-[#d0c4bb] hover:text-[var(--theme-text)]'}`}>Dashboard</a>
)}
{onAnalyzeClick && (
<button onClick={onAnalyzeClick} className="flex items-center gap-2 bg-[var(--theme-text)]/10 text-[var(--theme-text)] border border-[var(--theme-text)]/20 px-4 py-1.5 rounded-full text-[11px] font-bold tracking-tight hover:bg-[var(--theme-text)]/20 transition-all hover-scale">
<Upload className="w-3 h-3" /> Analyze New Media
</button>
)}
</div>
<div className="flex items-center gap-3">
{!isAuthenticated ? (
<>
<button
onClick={() => { setNavLoading(true); router.push('/login'); }}
disabled={navLoading}
className="hidden sm:flex items-center gap-2 bg-transparent text-[var(--theme-text)] border border-[var(--theme-text)]/30 px-6 py-2 rounded-full text-[11px] font-bold tracking-tight hover:bg-[var(--theme-text)]/5 hover-scale disabled:opacity-60 transition-opacity"
>
{navLoading ? (
<svg className="animate-spin w-3 h-3" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"/>
</svg>
) : null}
Register
</button>
<button
onClick={() => { setNavLoading(true); router.push('/login'); }}
disabled={navLoading}
className="hidden sm:flex items-center gap-2 bg-[var(--theme-text)] text-[var(--theme-bg)] px-6 py-2 rounded-full text-[11px] font-bold tracking-tight hover:shadow-[0_0_20px_rgba(253,232,214,0.3)] hover-scale disabled:opacity-60 transition-opacity"
>
{navLoading ? (
<svg className="animate-spin w-3 h-3" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"/>
</svg>
) : null}
Login
</button>
</>
) : (
<div
className="flex items-center gap-3 relative group"
ref={dropdownRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div
className="flex items-center gap-3 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => {
const newPinned = !dropdownPinned;
setDropdownPinned(newPinned);
setDropdownOpen(newPinned);
}}
>
<span className="hidden sm:block text-[var(--theme-text)] opacity-80 text-sm font-mono tracking-tight hover-scale">{user?.name || "User"}</span>
<div className="w-10 h-10 rounded-full border border-[var(--theme-border)] overflow-hidden bg-[var(--theme-text)]/5 flex items-center justify-center dash-border hover-scale">
{localAvatar ? (
<img src={localAvatar} alt="Avatar" className="w-full h-full object-cover" />
) : (
<span className="text-[var(--theme-text)] font-bold text-sm">{(user?.name || "U").charAt(0).toUpperCase()}</span>
)}
</div>
</div>
{dropdownOpen && (
<div className="absolute right-0 top-full pt-3 w-56 flex flex-col z-[100] !cursor-none">
<div className="bg-[var(--theme-bg)] border border-[var(--theme-border)] rounded-xl py-2 shadow-2xl flex flex-col w-full h-full">
<button onClick={() => { setDropdownOpen(false); router.push('/dashboard'); }} className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none">
<LayoutDashboard className="w-4 h-4 !cursor-none" /> Dashboard
</button>
<button onClick={() => { setDropdownOpen(false); router.push('/profile'); }} className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none">
<UserCircle className="w-4 h-4 !cursor-none" /> My Profile
</button>
{localAvatar && (
<button
className="px-4 py-3 flex items-center gap-3 text-sm text-yellow-500/80 hover:text-yellow-500 hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none"
onClick={handleRemoveAvatar}
>
<Trash2 className="w-4 h-4 !cursor-none" /> Remove Avatar
</button>
)}
<div className="h-px w-full bg-[var(--theme-border)] my-1"></div>
<button
onClick={toggleSound}
className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left justify-between !cursor-none"
>
<div className="flex items-center gap-3">
{soundEnabled ? <Volume2 className="w-4 h-4 !cursor-none text-green-400" /> : <VolumeX className="w-4 h-4 !cursor-none text-red-400" />}
Alerts Sound
</div>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider ${soundEnabled ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}`}>
{soundEnabled ? 'ON' : 'OFF'}
</span>
</button>
<div className="h-px w-full bg-[var(--theme-border)] my-1"></div>
<button
className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none"
onClick={() => {
setDropdownOpen(false);
setFeedbackModalOpen(true);
}}
>
<MessageSquare className="w-4 h-4 !cursor-none" /> Report Issue
</button>
<div className="h-px w-full bg-[var(--theme-border)] my-1"></div>
<button
className="px-4 py-3 flex items-center gap-3 text-sm text-red-400 hover:text-red-300 hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none"
onClick={() => {
setDropdownOpen(false);
logout();
}}
>
<LogOut className="w-4 h-4 !cursor-none" /> Logout
</button>
</div>
</div>
)}
</div>
)}
<button className="lg:hidden text-[var(--theme-text)] p-2" id="menu-toggle" onClick={() => setMobileMenuOpen(true)}>
<Menu className="w-6 h-6" />
</button>
</div>
</div>
</nav>
{/* Mobile Menu */}
<div className={`fixed inset-0 z-[70] bg-[#0d0e13] flex flex-col items-center justify-center p-12 text-center transition-all duration-500 ${mobileMenuOpen ? 'opacity-100 visible pointer-events-auto translate-y-0' : 'opacity-0 invisible pointer-events-none -translate-y-[20px]'}`} id="mobile-menu">
<button className="absolute top-8 right-8 text-[var(--theme-text)] pointer-events-auto" id="menu-close" onClick={() => setMobileMenuOpen(false)}>
<X className="w-10 h-10" />
</button>
<div className="flex flex-col gap-8 pointer-events-auto">
<a className="menu-link text-4xl font-black uppercase tracking-tighter text-[var(--theme-text)] hover:text-[var(--theme-text)]/70 transition-colors" href="/">Platform</a>
{isAuthenticated && (
<a onClick={() => { setMobileMenuOpen(false); router.push('/dashboard'); }} className="cursor-pointer menu-link text-4xl font-black uppercase tracking-tighter text-[var(--theme-border)] hover:text-[var(--theme-text)]/70 transition-colors">Dashboard</a>
)}
</div>
</div>
<FeedbackModal isOpen={feedbackModalOpen} onClose={() => setFeedbackModalOpen(false)} />
</>
);
}