'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { useRouter, usePathname } from 'next/navigation' import Image from 'next/image' import { Dialog, DialogPanel, Disclosure, DisclosureButton, DisclosurePanel, Popover, PopoverButton, PopoverGroup, PopoverPanel, Transition, } from '@headlessui/react' import { Bars3Icon, UserCircleIcon, XMarkIcon, ArrowRightOnRectangleIcon, } from '@heroicons/react/24/outline' import { ChevronDownIcon } from '@heroicons/react/20/solid' import useAuthStore from '../../store/authStore' import { Avatar } from '../avatar' // ENV-BASED FEATURE FLAGS (string envs: treat "false" as off, everything else as on) const DISPLAY_NEWS = process.env.NEXT_PUBLIC_DISPLAY_NEWS !== 'false' const DISPLAY_MEMBERSHIP = process.env.NEXT_PUBLIC_DISPLAY_MEMBERSHIP !== 'false' const DISPLAY_ABOUT_US = process.env.NEXT_PUBLIC_DISPLAY_ABOUT_US !== 'false' const DISPLAY_MATRIX = process.env.NEXT_PUBLIC_DISPLAY_MATRIX !== 'false' const DISPLAY_ABONEMENTS = process.env.NEXT_PUBLIC_DISPLAY_ABONEMENTS !== 'false' const DISPLAY_POOLS = process.env.NEXT_PUBLIC_DISPLAY_POOLS !== 'false' // Information dropdown, controlled by env flags const informationItems = [ { name: 'Affiliate-Links', href: '/affiliate-links', description: 'Browse our partner links' }, ...(DISPLAY_MEMBERSHIP ? [{ name: 'Memberships', href: '/memberships', description: 'Explore membership options' }] : []), ...(DISPLAY_ABOUT_US ? [{ name: 'About us', href: '/about-us', description: 'Learn more about us' }] : []), ] // Top-level navigation links, controlled by env flags const navLinks = [ ...(DISPLAY_NEWS ? [{ name: 'News', href: '/news' }] : []), ] // Toggle visibility of Shop navigation across header (desktop + mobile) const showShop = false interface HeaderProps { setGlobalLoggingOut?: (value: boolean) => void; // NEW } export default function Header({ setGlobalLoggingOut }: HeaderProps) { const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [mounted, setMounted] = useState(false) const [animateIn, setAnimateIn] = useState(false) const [scrollY, setScrollY] = useState(0) const [isMobile, setIsMobile] = useState(false) const user = useAuthStore(s => s.user) const logout = useAuthStore(s => s.logout) const accessToken = useAuthStore(s => s.accessToken) const refreshAuthToken = useAuthStore(s => s.refreshAuthToken) const router = useRouter() const pathname = usePathname() const isParallaxPage = pathname === '/' || pathname === '/login' || pathname === '/password-reset' || pathname === '/register' const parallaxEnabled = isParallaxPage && !isMobile const headerIsFixedOverlay = isParallaxPage && !isMobile const headerPositionClass = isParallaxPage ? (isMobile ? 'sticky top-0 w-full' : 'fixed top-0 left-0 w-full') : 'relative' const [hasReferralPerm, setHasReferralPerm] = useState(false) const [adminMgmtOpen, setAdminMgmtOpen] = useState(false) const managementRef = useRef(null) const [canSeeDashboard, setCanSeeDashboard] = useState(false) const headerElRef = useRef(null) const handleLogout = async () => { try { // start global logout transition setGlobalLoggingOut?.(true) await logout() setMobileMenuOpen(false) router.push('/login') } catch (err) { console.error('Logout failed:', err) setGlobalLoggingOut?.(false) } }; // Helper to get user initials for profile icon const getUserInitials = () => { if (!user) return 'U'; if (user.firstName || user.lastName) { return ( (user.firstName?.[0] || '') + (user.lastName?.[0] || '') ).toUpperCase(); } if (user.email) { return user.email[0].toUpperCase(); } return 'U'; }; // Initial theme (dark/light) + mark mounted + start header animation useEffect(() => { const stored = localStorage.getItem('theme') const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches const useDark = stored === 'dark' || (!stored && prefersDark) if (useDark) { document.documentElement.classList.add('dark') } else { document.documentElement.classList.remove('dark') } setMounted(true) setAnimateIn(true) }, []) // Detect mobile devices (for disabling parallax) useEffect(() => { const mq = window.matchMedia('(max-width: 768px)') const apply = () => setIsMobile(mq.matches) apply() mq.addEventListener?.('change', apply) window.addEventListener('resize', apply, { passive: true }) return () => { mq.removeEventListener?.('change', apply) window.removeEventListener('resize', apply) } }, []) // Home + login scroll listener: reveal header after first scroll with slight parallax useEffect(() => { if (!mounted) return if (!parallaxEnabled) { // non-parallax (and mobile): header always visible, no scroll listeners setScrollY(100) return } const handleScroll = () => { const y = window.scrollY || window.pageYOffset || 0 setScrollY(y) } const handleWheel = (e: WheelEvent) => { // virtual scroll so header can reveal even if page cannot scroll setScrollY(prev => { const next = prev + e.deltaY return Math.max(0, Math.min(next, 200)) }) } window.addEventListener('scroll', handleScroll, { passive: true }) window.addEventListener('wheel', handleWheel, { passive: true }) return () => { window.removeEventListener('scroll', handleScroll) window.removeEventListener('wheel', handleWheel) } }, [mounted, parallaxEnabled]) // Fetch user permissions and set hasReferralPerm useEffect(() => { let cancelled = false const fetchPermissions = async () => { if (!mounted) { console.log('⏸️ Header: not mounted yet, skipping permissions fetch') return } if (!user) { console.log('ℹ️ Header: no user, clearing permission flag') if (!cancelled) setHasReferralPerm(false) return } const uid = (user as any)?.id ?? (user as any)?._id ?? (user as any)?.userId if (!uid) { console.warn('⚠️ Header: user id missing, cannot fetch permissions', user) if (!cancelled) setHasReferralPerm(false) return } const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' const url = `${base}/api/users/${uid}/permissions` console.log('🌐 Header: fetching permissions:', { url, uid }) // Ensure we have a token (try refresh if needed) let tokenToUse = accessToken try { if (!tokenToUse && refreshAuthToken) { const ok = await refreshAuthToken() if (ok) tokenToUse = useAuthStore.getState().accessToken } } catch (e) { console.error('❌ Header: refreshAuthToken error:', e) } try { const res = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json', ...(tokenToUse ? { Authorization: `Bearer ${tokenToUse}` } : {}) } }) console.log('📡 Header: permissions status:', res.status) const body = await res.json().catch(() => null) console.log('📦 Header: permissions body:', body) // Try common shapes const permsSrc = body?.data?.permissions ?? body?.permissions ?? body let can = false if (Array.isArray(permsSrc)) { // Could be array of strings or objects can = permsSrc.includes?.('can_create_referrals') || permsSrc.some?.((p: any) => p?.name === 'can_create_referrals' || p?.key === 'can_create_referrals') } else if (permsSrc && typeof permsSrc === 'object') { can = !!permsSrc.can_create_referrals } console.log('✅ Header: can_create_referrals =', can) if (!cancelled) setHasReferralPerm(!!can) } catch (e) { console.error('❌ Header: fetch permissions error:', e) if (!cancelled) setHasReferralPerm(false) } } fetchPermissions() return () => { cancelled = true } }, [mounted, user, accessToken, refreshAuthToken]) // NEW: fetch onboarding status to decide if dashboard should be visible useEffect(() => { let cancelled = false const fetchOnboardingStatus = async () => { if (!mounted || !user) { if (!cancelled) setCanSeeDashboard(false) return } let tokenToUse = accessToken try { if (!tokenToUse && refreshAuthToken) { const ok = await refreshAuthToken() if (ok) tokenToUse = useAuthStore.getState().accessToken } } catch (e) { console.error('❌ Header: refreshAuthToken (status-progress) error:', e) } if (!tokenToUse) { if (!cancelled) setCanSeeDashboard(false) return } try { const statusUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/user/status-progress` console.log('🌐 Header: fetching status-progress:', statusUrl) const res = await fetch(statusUrl, { method: 'GET', headers: { Authorization: `Bearer ${tokenToUse}`, 'Content-Type': 'application/json', }, credentials: 'include', }) if (!res.ok) { console.warn('⚠️ Header: status-progress failed with', res.status) if (!cancelled) setCanSeeDashboard(false) return } const statusData = await res.json().catch(() => null) const progressData = statusData?.progress || statusData || {} const steps = progressData.steps || [] const allStepsCompleted = steps.length === 4 && steps.every((step: any) => step?.completed === true) const isActive = progressData.status === 'active' if (!cancelled) { setCanSeeDashboard(allStepsCompleted && isActive) } } catch (e) { console.error('❌ Header: status-progress fetch error:', e) if (!cancelled) setCanSeeDashboard(false) } } fetchOnboardingStatus() return () => { cancelled = true } }, [mounted, user, accessToken, refreshAuthToken]) const isLoggedIn = !!user const userPresent = mounted && isLoggedIn // NEW: detect admin role across common shapes (guarded by mount to avoid SSR/CSR mismatch) const rawIsAdmin = !!user && ( (user as any)?.role === 'admin' || (user as any)?.userType === 'admin' || (user as any)?.isAdmin === true || ((user as any)?.roles?.includes?.('admin')) ) const isAdmin = mounted && rawIsAdmin // Only gate visibility by scroll on parallax-enabled pages const headerVisible = parallaxEnabled ? animateIn && scrollY > 24 : animateIn const parallaxOffset = parallaxEnabled ? Math.max(-16, -scrollY * 0.15) : 0 // When the fixed header becomes visible, expose its height as a CSS variable so // pages (e.g. /register) can pad their content and avoid being overlapped. useEffect(() => { if (!mounted) return let raf1 = 0 let raf2 = 0 const applySpacer = () => { // Only reserve space when header is fixed/overlaying content. if (!headerIsFixedOverlay) { document.documentElement.style.setProperty('--pp-header-spacer', '0px') return } const h = headerElRef.current?.getBoundingClientRect().height ?? 0 const spacer = headerVisible ? `${Math.ceil(h)}px` : '0px' document.documentElement.style.setProperty('--pp-header-spacer', spacer) } const applyShiftFade = () => { if (!headerVisible) { document.documentElement.style.setProperty('--pp-page-shift-opacity', '1') return } // Less noticeable dip to avoid "too translucent" look during the shift. document.documentElement.style.setProperty('--pp-page-shift-opacity', '0.99') raf1 = window.requestAnimationFrame(() => { raf2 = window.requestAnimationFrame(() => { document.documentElement.style.setProperty('--pp-page-shift-opacity', '1') }) }) } applySpacer() applyShiftFade() const onResize = () => applySpacer() window.addEventListener('resize', onResize, { passive: true }) return () => { window.removeEventListener('resize', onResize) if (raf1) cancelAnimationFrame(raf1) if (raf2) cancelAnimationFrame(raf2) } }, [mounted, headerVisible, headerIsFixedOverlay]) // Hard cleanup: if any scroll-lock left padding/overflow on /, remove it when the drawer is closed. useEffect(() => { if (mobileMenuOpen) return const html = document.documentElement const body = document.body body.style.paddingRight = '' html.style.paddingRight = '' if (body.style.overflow === 'hidden') body.style.overflow = '' if (html.style.overflow === 'hidden') html.style.overflow = '' }, [mobileMenuOpen]) return (
{/* Admin subheader (gold) - centered */} {userPresent && isAdmin && (
Admin Navigation {/* Updated Management dropdown */}
{adminMgmtOpen && (
{DISPLAY_MATRIX && ( )} {DISPLAY_ABONEMENTS && ( <> )} {DISPLAY_POOLS && ( )} {DISPLAY_NEWS && ( )}
)}
)} {/* Side drawer menu: mobile + desktop */} {/* Overlay: smoother, longer fade-out */}
{/* Sliding panel: smoother, longer close animation */} {/* Header row */}
{/* Scrollable content */}
{!mounted ? ( // ...existing skeleton...
) : user ? ( <> {/* User info + basic nav */}
{user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : (user?.email || 'User')}
{user?.email || 'user@example.com'}
{canSeeDashboard && ( )}
{/* Main navigation (info + links + referral + ADMIN LAST) */}
{/* Information disclosure */} Information {informationItems.map(item => ( { router.push(item.href); setMobileMenuOpen(false); }} className="block rounded-lg py-2 pl-6 pr-3 text-sm/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left" > {item.name} ))} {/* Navigation Links */} {navLinks.map((link) => ( ))} {/* Referral / matrix / abonnements */} {hasReferralPerm && ( <> {DISPLAY_MATRIX && ( )} {DISPLAY_ABONEMENTS && ( )} )} {/* Admin navigation – LAST, neutral glassy with pulsating hover */} {isAdmin && (

Admin Navigation

{DISPLAY_MATRIX && ( )} {DISPLAY_ABONEMENTS && ( <> )} {DISPLAY_POOLS && ( )} {DISPLAY_NEWS && ( )}
)}
) : ( // logged-out drawer
{/* Information disclosure */} Information {informationItems.map(item => ( { router.push(item.href); setMobileMenuOpen(false); }} className="block rounded-lg py-2 pl-6 pr-3 text-sm/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left" > {item.name} ))} {/* Navigation Links */} {navLinks.map((link) => ( ))}
)}
{/* Sticky bottom logout button with pulsating hover */} {user && (
)}
) }