'use client'; import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; import Image from 'next/image'; import { useState, createElement, useEffect, useMemo, useCallback, memo } from 'react'; import { Search, Activity, HelpCircle, Download, ChevronDown, User, Shield, Database, CreditCard, LogOut, LucideIcon } from 'lucide-react'; import { logout, UserProfile, checkApiKeyStatus } from '@/utils/api'; import { useAuth } from '@/utils/auth'; const ANIMATION_DURATION = { SIDEBAR: 500, TEXT: 300, SUBMENU: 500, ICON_HOVER: 200, COLOR_TRANSITION: 200, HOVER_SCALE: 200, } as const; const DIMENSIONS = { SIDEBAR_EXPANDED: 220, SIDEBAR_COLLAPSED: 64, ICON_SIZE: 18, USER_AVATAR_SIZE: 32, HEADER_HEIGHT: 64, } as const; const ANIMATION_DELAYS = { BASE: 0, INCREMENT: 50, TEXT_BASE: 250, SUBMENU_INCREMENT: 30, } as const; interface NavigationItem { name: string; href?: string; action?: () => void; icon: LucideIcon | string; isLucide: boolean; hasSubmenu?: boolean; ariaLabel?: string; } interface SubmenuItem { name: string; href: string; icon: LucideIcon | string; isLucide: boolean; ariaLabel?: string; } interface SidebarProps { isCollapsed: boolean; onToggle: (collapsed: boolean) => void; onSearchClick?: () => void; } interface AnimationStyles { text: React.CSSProperties; submenu: React.CSSProperties; sidebarContainer: React.CSSProperties; textContainer: React.CSSProperties; } const useAnimationStyles = (isCollapsed: boolean) => { const [isAnimating, setIsAnimating] = useState(false); useEffect(() => { setIsAnimating(true); const timer = setTimeout(() => setIsAnimating(false), ANIMATION_DURATION.SIDEBAR); return () => clearTimeout(timer); }, [isCollapsed]); const getTextAnimationStyle = useCallback( (delay = 0): React.CSSProperties => ({ willChange: 'opacity', transition: `opacity ${ANIMATION_DURATION.TEXT}ms ease-out`, transitionDelay: `${delay}ms`, opacity: isCollapsed ? 0 : 1, pointerEvents: isCollapsed ? 'none' : 'auto', }), [isCollapsed] ); const getSubmenuAnimationStyle = useCallback( (isExpanded: boolean): React.CSSProperties => ({ willChange: 'opacity, max-height', transition: `all ${ANIMATION_DURATION.SUBMENU}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`, maxHeight: isCollapsed || !isExpanded ? '0px' : '400px', opacity: isCollapsed || !isExpanded ? 0 : 1, }), [isCollapsed] ); const sidebarContainerStyle: React.CSSProperties = useMemo( () => ({ willChange: 'width', transition: `width ${ANIMATION_DURATION.SIDEBAR}ms cubic-bezier(0.4, 0, 0.2, 1)`, }), [] ); const getTextContainerStyle = useCallback( (): React.CSSProperties => ({ width: isCollapsed ? '0px' : '150px', overflow: 'hidden', transition: `width ${ANIMATION_DURATION.SIDEBAR}ms cubic-bezier(0.4, 0, 0.2, 1)`, }), [isCollapsed] ); const getUniformTextStyle = useCallback( (): React.CSSProperties => ({ willChange: 'opacity', opacity: isCollapsed ? 0 : 1, transition: `opacity 300ms ease ${isCollapsed ? '0ms' : '200ms'}`, whiteSpace: 'nowrap' as const, }), [isCollapsed] ); return { isAnimating, getTextAnimationStyle, getSubmenuAnimationStyle, sidebarContainerStyle, getTextContainerStyle, getUniformTextStyle, }; }; const IconComponent = memo<{ icon: LucideIcon | string; isLucide: boolean; alt: string; className?: string; }>(({ icon, isLucide, alt, className = 'h-[18px] w-[18px] transition-transform duration-200' }) => { if (isLucide) { return createElement(icon as LucideIcon, { className, 'aria-hidden': true }); } return {alt}; }); IconComponent.displayName = 'IconComponent'; const SidebarComponent = ({ isCollapsed, onToggle, onSearchClick }: SidebarProps) => { const pathname = usePathname(); const router = useRouter(); const [isSettingsExpanded, setIsSettingsExpanded] = useState(pathname.startsWith('/settings')); const { user: userInfo, isLoading: authLoading } = useAuth(); const [hasApiKey, setHasApiKey] = useState(null); const { isAnimating, getTextAnimationStyle, getSubmenuAnimationStyle, sidebarContainerStyle, getTextContainerStyle, getUniformTextStyle } = useAnimationStyles(isCollapsed); useEffect(() => { checkApiKeyStatus() .then(status => setHasApiKey(status.hasApiKey)) .catch(err => { console.error('Failed to check API key status:', err); setHasApiKey(null); // Set to null on error }); }, []); useEffect(() => { if (pathname.startsWith('/settings')) { setIsSettingsExpanded(true); } }, [pathname]); const navigation = useMemo( () => [ { name: 'Search', action: onSearchClick, icon: '/search.svg', isLucide: false, ariaLabel: 'Open search', }, { name: 'My Activity', href: '/activity', icon: '/activity.svg', isLucide: false, ariaLabel: 'View my activity', }, { name: 'Personalize', href: '/personalize', icon: '/book.svg', isLucide: false, ariaLabel: 'Personalization settings', }, { name: 'Settings', href: '/settings', icon: '/setting.svg', isLucide: false, hasSubmenu: true, ariaLabel: 'Settings menu', }, ], [onSearchClick] ); const settingsSubmenu = useMemo( () => [ { name: 'Personal Profile', href: '/settings', icon: '/user.svg', isLucide: false, ariaLabel: 'Personal profile settings' }, { name: 'Data & privacy', href: '/settings/privacy', icon: '/privacy.svg', isLucide: false, ariaLabel: 'Data and privacy settings' }, { name: 'Billing', href: '/settings/billing', icon: '/credit-card.svg', isLucide: false, ariaLabel: 'Billing settings' }, ], [] ); const bottomItems = useMemo( () => [ { href: 'https://discord.gg/UCZH5B5Hpd', icon: '/linkout.svg', text: 'Join Discord', ariaLabel: 'Help Center (new window)', }, { href: 'https://www.dropbox.com/scl/fi/esk4h8z45sryvbremy57v/Pickle_latest.dmg?rlkey=92y535bz6p6gov6vd17x6q53b&st=9kl0annj&dl=1', icon: '/download.svg', text: 'Download Pickle Camera', ariaLabel: 'Download Pickle Camera (new window)', }, { href: 'hhttps://www.dropbox.com/scl/fi/znid09apxiwtwvxer6oc9/Glass_latest.dmg?rlkey=gwvvyb3bizkl25frhs4k1zwds&st=37q31b4w&dl=1', icon: '/download.svg', text: 'Download Pickle Glass', ariaLabel: 'Download Pickle Glass (new window)', }, ], [] ); const toggleSidebar = useCallback(() => { onToggle(!isCollapsed); }, [isCollapsed, onToggle]); const toggleSettings = useCallback(() => { if (!pathname.startsWith('/settings')) { setIsSettingsExpanded(prev => !prev); } }, [pathname]); const handleLogout = useCallback(async () => { try { await logout(); } catch (error) { console.error('An error occurred during logout:', error); } }, []); const handleKeyDown = useCallback((event: React.KeyboardEvent, action?: () => void) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); action?.(); } }, []); const renderNavigationItem = useCallback( (item: NavigationItem, index: number) => { const isActive = item.href ? pathname.startsWith(item.href) : false; const animationDelay = 0; const baseButtonClasses = ` group flex items-center rounded-[8px] px-[12px] py-[10px] text-[14px] text-[#282828] w-full relative transition-colors duration-${ANIMATION_DURATION.COLOR_TRANSITION} ease-out focus:outline-none `; const getStateClasses = (isActive: boolean) => isActive ? 'bg-[#f2f2f2] text-[#282828]' : 'text-[#282828] hover:text-[#282828] hover:bg-[#f7f7f7]'; if (item.action) { return (
  • ); } if (item.hasSubmenu) { return (
    • {settingsSubmenu.map((subItem, subIndex) => (
    • {subItem.name}
    • ))}
    • {isFirebaseUser ? ( ) : (
  • ); } return (
  • {item.name}
  • ); }, [ pathname, isCollapsed, isSettingsExpanded, toggleSettings, handleLogout, handleKeyDown, getUniformTextStyle, getTextContainerStyle, getSubmenuAnimationStyle, settingsSubmenu, ] ); const getUserDisplayName = useCallback(() => { if (authLoading) return 'Loading...'; return userInfo?.display_name || 'Guest'; }, [userInfo, authLoading]); const getUserInitial = useCallback(() => { if (authLoading) return 'L'; return userInfo?.display_name ? userInfo.display_name.charAt(0).toUpperCase() : 'G'; }, [userInfo, authLoading]); const isFirebaseUser = userInfo && userInfo.uid !== 'default_user'; return ( ); }; const Sidebar = memo(SidebarComponent); Sidebar.displayName = 'Sidebar'; export default Sidebar;