profit-planet-frontend/src/app/components/nav/Header.tsx
2026-01-14 16:16:38 +01:00

1033 lines
45 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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<HTMLDivElement | null>(null)
const [canSeeDashboard, setCanSeeDashboard] = useState(false)
const headerElRef = useRef<HTMLElement | null>(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 <body>/<html>, 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 (
<header
ref={headerElRef}
className={`${headerPositionClass} isolate z-30 shadow-lg shadow-black/30 after:pointer-events-none after:absolute after:inset-0 after:-z-10 after:bg-[radial-gradient(circle_at_20%_20%,rgba(56,124,255,0.18),transparent_55%),radial-gradient(circle_at_80%_35%,rgba(139,92,246,0.16),transparent_60%)] ${
isAdmin ? '' : 'border-b border-white/10'
} ${
headerVisible ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-6 pointer-events-none'
} transition-all duration-500 ease-out`}
style={{
background: 'linear-gradient(135deg, #0F1D37 0%, #0A162A 50%, #081224 100%)',
...(parallaxEnabled ? { transform: `translateY(${parallaxOffset}px)` } : {}),
}}
>
<nav
aria-label="Global"
className="mx-auto flex max-w-7xl items-center justify-between p-6 lg:px-8"
>
<div className="flex lg:flex-1">
<button
onClick={() => router.push('/')}
className="p-2 flex items-center gap-3 max-w-full lg:-m-1.5 lg:gap-0"
>
<span className="sr-only">ProfitPlanet</span>
<Image
src="/images/logos/pp_logo_gold_transparent.png"
alt="ProfitPlanet Logo"
width={280}
height={84}
className="h-14 w-auto flex-shrink-0 sm:h-16 lg:h-[4.5rem]"
/>
{/* Removed flickering mobile heading (now only shown inside the sliding panel) */}
</button>
</div>
{/* Mobile hamburger (only on small screens) */}
<div className="flex lg:hidden">
<button
type="button"
onClick={() => setMobileMenuOpen(open => !open)}
aria-expanded={mobileMenuOpen}
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-400 transition-transform duration-300 ease-out data-[open=true]:rotate-90"
data-open={mobileMenuOpen ? 'true' : 'false'}
>
<span className="sr-only">
{mobileMenuOpen ? 'Close main menu' : 'Open main menu'}
</span>
<span className="relative flex h-6 w-6 items-center justify-center">
<Bars3Icon
aria-hidden="true"
className={`size-6 transition-all duration-300 ease-out ${
mobileMenuOpen
? 'opacity-0 -rotate-90 scale-75'
: 'opacity-100 rotate-0 scale-100'
}`}
/>
<XMarkIcon
aria-hidden="true"
className={`pointer-events-none absolute size-6 transition-all duration-300 ease-out ${
mobileMenuOpen
? 'opacity-100 rotate-0 scale-100'
: 'opacity-0 rotate-90 scale-75'
}`}
/>
</span>
</button>
</div>
<PopoverGroup className="hidden lg:flex lg:gap-x-12">
{/* Navigation Links */}
{navLinks.map((link) => (
<button
key={link.href}
onClick={() => router.push(link.href)}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
{link.name}
</button>
))}
{/* Conditional user-specific links (top navbar) */}
{userPresent && hasReferralPerm && (
<>
{/* Referral Management REMOVED from top nav */}
{DISPLAY_MATRIX && (
<button
onClick={() => router.push('/personal-matrix')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
Personal Matrix
</button>
)}
{DISPLAY_ABONEMENTS && (
<button
onClick={() => router.push('/coffee-abonnements')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
Coffee Abonnements
</button>
)}
</>
)}
{/* Information dropdown already removed here */}
</PopoverGroup>
<div className="hidden lg:flex lg:flex-1 lg:justify-end lg:items-center lg:gap-x-4">
{/* Auth slot */}
<div className="flex items-center">
{userPresent ? (
<Popover className="relative">
<PopoverButton className="flex items-center gap-x-1 text-sm font-semibold text-gray-900 dark:text-white">
<Avatar
src=""
initials={(() => {
if (!user) return 'U'
if (user.firstName || user.lastName) {
return ((user.firstName?.[0] || '') + (user.lastName?.[0] || '')).toUpperCase()
}
return user.email ? user.email[0].toUpperCase() : 'U'
})()}
className="size-8 bg-gradient-to-br from-indigo-500/40 to-indigo-600/60 text-white"
/>
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none text-gray-500" />
</PopoverButton>
<PopoverPanel
transition
className="absolute left-0 top-full mt-2 w-64 rounded-md bg-white dark:bg-gray-900 ring-1 ring-black/10 dark:ring-white/10 shadow-lg data-closed:-translate-y-1 data-closed:opacity-0 data-enter:duration-200 data-leave:duration-150"
>
<div className="p-4">
<div className="flex flex-col border-b border-gray-200 dark:border-white/10 pb-4 mb-4">
<div className="font-medium text-gray-900 dark:text-white">
{user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : (user?.email || 'User')}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{user?.email || 'user@example.com'}
</div>
</div>
{canSeeDashboard && (
<button
onClick={() => router.push('/dashboard')}
className="flex items-center gap-x-2 w-full text-left p-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-white/5 rounded-md"
>
<Bars3Icon className="size-5 text-gray-400" />
Dashboard
</button>
)}
<button
onClick={() => router.push('/profile')}
className="flex items-center gap-x-2 w-full text-left p-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-white/5 rounded-md"
>
<UserCircleIcon className="size-5 text-gray-400" />
Profile
</button>
{/* Logout removed from profile dropdown; still available in hamburger menu bottom */}
</div>
</PopoverPanel>
</Popover>
) : mounted ? (
<button
onClick={() => router.push('/login')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
Log in <span aria-hidden="true">&rarr;</span>
</button>
) : (
<div aria-hidden="true" className="w-20 h-8 rounded-md bg-gray-200 dark:bg-gray-700/70 animate-pulse" />
)}
</div>
{/* Desktop hamburger (right side, next to login/profile) */}
<button
type="button"
onClick={() => setMobileMenuOpen(open => !open)}
aria-expanded={mobileMenuOpen}
className="inline-flex items-center justify-center rounded-md p-2.5 text-gray-300 hover:text-white hover:bg-white/10 transition-colors"
>
<span className="sr-only">
{mobileMenuOpen ? 'Close main menu' : 'Open main menu'}
</span>
<span className="relative flex h-6 w-6 items-center justify-center">
<Bars3Icon
aria-hidden="true"
className={`size-6 transition-all duration-300 ease-out ${
mobileMenuOpen
? 'opacity-0 -rotate-90 scale-75'
: 'opacity-100 rotate-0 scale-100'
}`}
/>
<XMarkIcon
aria-hidden="true"
className={`pointer-events-none absolute size-6 transition-all duration-300 ease-out ${
mobileMenuOpen
? 'opacity-100 rotate-0 scale-100'
: 'opacity-0 rotate-90 scale-75'
}`}
/>
</span>
</button>
</div>
</nav>
{/* Admin subheader (gold) - centered */}
{userPresent && isAdmin && (
<div
className="w-full border-t border-amber-700/40"
style={{ background: 'linear-gradient(90deg, #D4AF37 0%, #C99A2E 100%)' }}
>
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="flex flex-wrap items-center justify-center gap-5 py-2">
<span className="text-xs font-semibold uppercase tracking-wide text-[#3B2C04]/80">
Admin Navigation
</span>
<button
onClick={() => { router.push('/admin') }}
className="text-sm font-semibold text-[#0F1D37] hover:text-[#7A5E1A]"
>
Dashboard
</button>
<button
onClick={() => { router.push('/admin/user-verify') }}
className="text-sm font-semibold text-[#0F1D37] hover:text-[#7A5E1A]"
>
User Verify
</button>
{/* Updated Management dropdown */}
<div ref={managementRef} className="relative">
<button
onClick={() => setAdminMgmtOpen(o => !o)}
aria-haspopup="true"
aria-expanded={adminMgmtOpen}
className="text-sm font-semibold text-[#0F1D37] hover:text-[#7A5E1A] flex items-center gap-1"
>
Management
<ChevronDownIcon
className={`h-4 w-4 transition-transform ${adminMgmtOpen ? 'rotate-180' : ''}`}
/>
</button>
{adminMgmtOpen && (
<div
className="absolute left-1/2 -translate-x-1/2 mt-2 min-w-[15rem] rounded-md bg-white shadow-lg ring-1 ring-black/10 z-50"
role="menu"
>
<div className="py-2">
<button
onClick={() => { router.push('/admin/user-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
User Management
</button>
{DISPLAY_MATRIX && (
<button
onClick={() => { router.push('/admin/matrix-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
Matrix Management
</button>
)}
<button
onClick={() => { router.push('/admin/contract-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
Contract Management
</button>
{DISPLAY_ABONEMENTS && (
<>
<button
onClick={() => { router.push('/admin/subscriptions'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
Coffee Management
</button>
<button
onClick={() => { router.push('/admin/finance-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
Finance Management
</button>
</>
)}
{DISPLAY_POOLS && (
<button
onClick={() => { router.push('/admin/pool-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
Pool Management
</button>
)}
<button
onClick={() => { router.push('/admin/affiliate-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
Affiliate Management
</button>
{DISPLAY_NEWS && (
<button
onClick={() => { router.push('/admin/news-management'); setAdminMgmtOpen(false); }}
className="w-full text-left px-4 py-2 text-sm text-[#0F1D37] hover:bg-[#F5F3EE]"
role="menuitem"
>
News Management
</button>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* Side drawer menu: mobile + desktop */}
<Dialog open={mobileMenuOpen} onClose={setMobileMenuOpen}>
<Transition appear show={mobileMenuOpen}>
{/* Overlay: smoother, longer fade-out */}
<Transition.Child
enter="transition-opacity duration-400 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-800 ease-out"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-40 bg-black/40 backdrop-blur-sm" />
</Transition.Child>
{/* Sliding panel: smoother, longer close animation */}
<Transition.Child
enter="transform transition-all duration-500 ease-out"
enterFrom="translate-x-full opacity-0 scale-95"
enterTo="translate-x-0 opacity-100 scale-100"
leave="transform transition-all duration-800 ease-in-out"
leaveFrom="translate-x-0 opacity-100 scale-100"
leaveTo="translate-x-full opacity-0 scale-95"
>
<DialogPanel className="fixed inset-y-0 right-0 z-50 w-full sm:max-w-sm h-full bg-white/95 dark:bg-gray-900/95 backdrop-blur-xl sm:ring-1 sm:ring-black/10 dark:sm:ring-gray-100/10 shadow-[0_0_40px_rgba(0,0,0,0.55)] flex flex-col">
{/* Header row */}
<div className="flex items-center justify-between px-1.5 pt-1.5 pb-2">
<button
onClick={() => router.push('/')}
className="p-1.5 flex items-center gap-3"
>
<span className="sr-only">ProfitPlanet</span>
<Image
src="/images/logos/pp_logo_gold_transparent.png"
alt="ProfitPlanet Logo"
width={190}
height={60}
className="h-12 w-auto flex-shrink-0"
/>
<span className="text-xl font-bold tracking-tight text-[#D4AF37]">
Profit Planet
</span>
</button>
<button
type="button"
onClick={() => setMobileMenuOpen(false)}
className="rounded-md p-2.5 text-gray-400 hover:text-gray-100 hover:bg-white/10 transition-transform duration-300 hover:scale-110"
>
<span className="sr-only">Close menu</span>
<XMarkIcon aria-hidden="true" className="size-6" />
</button>
</div>
{/* Scrollable content */}
<div className="mt-4 flex-1 overflow-y-auto overflow-x-hidden">
<div className="-my-6 divide-y divide-gray-200 dark:divide-white/10">
{!mounted ? (
// ...existing skeleton...
<div className="py-6 px-3">
<div className="h-28 w-full rounded-lg bg-gray-200 dark:bg-gray-700 animate-pulse" />
</div>
) : user ? (
<>
{/* User info + basic nav */}
<div className="pt-6 space-y-2">
<div className="flex flex-col border-b border-white/10 pb-4 mb-4 px-3">
<div className="font-medium text-white">
{user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : (user?.email || 'User')}
</div>
<div className="text-sm text-gray-400">
{user?.email || 'user@example.com'}
</div>
</div>
{canSeeDashboard && (
<button
onClick={() => {
router.push('/dashboard')
setMobileMenuOpen(false)
}}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
Dashboard
</button>
)}
<button
onClick={() => {
router.push('/profile')
setMobileMenuOpen(false)
}}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
Profile
</button>
</div>
{/* Main navigation (info + links + referral + ADMIN LAST) */}
<div className="space-y-4 py-6 px-1">
{/* Information disclosure */}
<Disclosure as="div">
<DisclosureButton className="group flex w-full items-center justify-between rounded-lg py-2 px-3 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5">
Information
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none group-data-open:rotate-180" />
</DisclosureButton>
<DisclosurePanel className="mt-2 space-y-1">
{informationItems.map(item => (
<DisclosureButton
key={item.name}
as="button"
onClick={() => { 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}
</DisclosureButton>
))}
</DisclosurePanel>
</Disclosure>
{/* Navigation Links */}
{navLinks.map((link) => (
<button
key={link.href}
onClick={() => { router.push(link.href); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
{link.name}
</button>
))}
{/* Referral / matrix / abonnements */}
{hasReferralPerm && (
<>
<button
onClick={() => { console.log('🧭 Header Mobile: navigate to /referral-management'); router.push('/referral-management'); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
Referral Management
</button>
{DISPLAY_MATRIX && (
<button
onClick={() => { router.push('/personal-matrix'); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
Personal Matrix
</button>
)}
{DISPLAY_ABONEMENTS && (
<button
onClick={() => { router.push('/coffee-abonnements'); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
Coffee Abonnements
</button>
)}
</>
)}
{/* Admin navigation LAST, neutral glassy with pulsating hover */}
{isAdmin && (
<div className="group mt-2 rounded-2xl border border-white/15 bg-gradient-to-br from-white/5 via-white/10 to-white/5 bg-clip-padding backdrop-blur-md shadow-[0_18px_45px_rgba(0,0,0,0.45)] ring-1 ring-white/15 transition-transform transition-shadow duration-200 ease-out hover:-translate-y-0.5 hover:shadow-[0_22px_55px_rgba(0,0,0,0.6)]">
<div className="px-3 py-2.5 group-hover:animate-pulse">
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-200 mb-1.5">
Admin Navigation
</p>
<div className="h-px w-full bg-gradient-to-r from-transparent via-white/70 to-transparent opacity-80 mb-2 transition-opacity group-hover:opacity-100" />
<div className="grid grid-cols-1 gap-1.5 text-sm">
<button
onClick={() => { router.push('/admin'); setMobileMenuOpen(false); }}
className="w-full text-left rounded-lg px-2 py-1.5 text-gray-100 hover:bg-white/15 hover:text-white transition-colors"
>
Dashboard
</button>
<button
onClick={() => { router.push('/admin/user-verify'); setMobileMenuOpen(false); }}
className="w-full text-left rounded-lg px-2 py-1.5 text-gray-100 hover:bg-white/15 hover:text-white transition-colors"
>
User Verify
</button>
<button
onClick={() => { router.push('/admin/user-management'); setMobileMenuOpen(false); }}
className="w-full text-left rounded-lg px-2 py-1.5 text-gray-100 hover:bg-white/15 hover:text-white transition-colors"
>
User Management
</button>
{DISPLAY_MATRIX && (
<button
onClick={() => { router.push('/admin/matrix-management'); setMobileMenuOpen(false); }}
className="w-full text-left rounded-lg px-2 py-1.5 text-gray-100 hover:bg-white/15 hover:text-white transition-colors"
>
Matrix Management
</button>
)}
<button
onClick={() => { router.push('/admin/contract-management'); setMobileMenuOpen(false); }}
className="w-full text-left rounded-lg px-2 py-1.5 text-gray-100 hover:bg-white/15 hover:text-white transition-colors"
>
Contract Management
</button>
{DISPLAY_ABONEMENTS && (
<>
<button
onClick={() => { router.push('/admin/subscriptions'); setMobileMenuOpen(false); }}
className="w-full text-left rounded-lg px-2 py-1.5 text-gray-100 hover:bg-white/15 hover:text-white transition-colors"
>
Coffee Management
</button>
<button
onClick={() => { router.push('/admin/finance-management'); setMobileMenuOpen(false); }}
className="w-full text-left rounded-lg px-2 py-1.5 text-gray-100 hover:bg-white/15 hover:text-white transition-colors"
>
Finance Management
</button>
</>
)}
{DISPLAY_POOLS && (
<button
onClick={() => { router.push('/admin/pool-management'); setMobileMenuOpen(false); }}
className="w-full text-left rounded-lg px-2 py-1.5 text-gray-100 hover:bg-white/15 hover:text-white transition-colors"
>
Pool Management
</button>
)}
<button
onClick={() => { router.push('/admin/affiliate-management'); setMobileMenuOpen(false); }}
className="w-full text-left rounded-lg px-2 py-1.5 text-gray-100 hover:bg-white/15 hover:text-white transition-colors"
>
Affiliate Management
</button>
{DISPLAY_NEWS && (
<button
onClick={() => { router.push('/admin/news-management'); setMobileMenuOpen(false); }}
className="w-full text-left rounded-lg px-2 py-1.5 text-gray-100 hover:bg-white/15 hover:text-white transition-colors"
>
News Management
</button>
)}
</div>
</div>
</div>
)}
</div>
</>
) : (
// logged-out drawer
<div className="py-6 space-y-4 px-1">
{/* Information disclosure */}
<Disclosure as="div">
<DisclosureButton className="group flex w-full items-center justify-between rounded-lg py-2 px-3 textbase/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5">
Information
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none group-data-open:rotate-180" />
</DisclosureButton>
<DisclosurePanel className="mt-2 space-y-1">
{informationItems.map(item => (
<DisclosureButton
key={item.name}
as="button"
onClick={() => { 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}
</DisclosureButton>
))}
</DisclosurePanel>
</Disclosure>
{/* Navigation Links */}
{navLinks.map((link) => (
<button
key={link.href}
onClick={() => { router.push(link.href); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2 textbase/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
{link.name}
</button>
))}
<div className="px-3">
<button
onClick={() => { router.push('/login'); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2.5 textbase/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
Log in
</button>
</div>
</div>
)}
</div>
</div>
{/* Sticky bottom logout button with pulsating hover */}
{user && (
<div className="border-t border-gray-200/60 dark:border-white/10 px-4 py-3">
<button
onClick={() => { handleLogout(); setMobileMenuOpen(false); }}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-red-500/90 hover:bg-red-600 text-white py-2.5 text-sm font-semibold shadow-md shadow-red-900/30 transition-transform transition-colors duration-200 hover:animate-pulse"
>
<ArrowRightOnRectangleIcon className="h-5 w-5" />
<span>Logout</span>
</button>
</div>
)}
</DialogPanel>
</Transition.Child>
</Transition>
</Dialog>
</header>
)
}