882 lines
39 KiB
TypeScript
882 lines
39 KiB
TypeScript
'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,
|
||
PopoverGroup,
|
||
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 [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 (force dark) + mark mounted + start header animation
|
||
useEffect(() => {
|
||
document.documentElement.classList.add('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',
|
||
cache: 'no-store',
|
||
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
|
||
|
||
const rawIsSuperAdmin =
|
||
!!user &&
|
||
(
|
||
(user as any)?.role === 'super_admin' ||
|
||
(user as any)?.userType === 'super_admin' ||
|
||
(user as any)?.isSuperAdmin === true ||
|
||
((user as any)?.roles?.includes?.('super_admin'))
|
||
)
|
||
const isSuperAdmin = mounted && rawIsSuperAdmin
|
||
|
||
const isAdminOrSuper = mounted && (rawIsAdmin || rawIsSuperAdmin)
|
||
|
||
// 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>
|
||
|
||
{/* CHANGED: remove profile icon/popover from header; keep login (when logged out) + hamburger */}
|
||
<div className="hidden lg:flex lg:flex-1 lg:justify-end lg:items-center lg:gap-x-3">
|
||
{!userPresent && 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">→</span>
|
||
</button>
|
||
)}
|
||
|
||
{/* Desktop hamburger (right side) */}
|
||
<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>
|
||
|
||
{/* 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 ? (
|
||
<>
|
||
{/* CHANGED: include profile icon INSIDE hamburger menu */}
|
||
<div className="pt-6 space-y-2">
|
||
<div className="flex items-center gap-3 border-b border-gray-200 dark:border-white/10 pb-4 mb-4 px-3">
|
||
<Avatar
|
||
src=""
|
||
initials={getUserInitials()}
|
||
className="size-10 bg-gradient-to-br from-indigo-500/40 to-indigo-600/60 text-white"
|
||
/>
|
||
<div className="min-w-0">
|
||
<div className="font-medium text-gray-900 dark:text-white truncate">
|
||
{user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : (user?.email || 'User')}
|
||
</div>
|
||
<div className="text-sm text-gray-600 dark:text-gray-400 truncate">
|
||
{user?.email || 'user@example.com'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* NEW: show Quick Action Dashboard link when user cannot access /dashboard yet */}
|
||
{!canSeeDashboard && (
|
||
<button
|
||
onClick={() => {
|
||
router.push('/quickaction-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"
|
||
>
|
||
Startup Dashboard
|
||
</button>
|
||
)}
|
||
|
||
{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 */}
|
||
{isAdmin && (
|
||
<div className="group mt-2 rounded-2xl border border-indigo-100 bg-white shadow-[0_12px_28px_rgba(15,23,42,0.12)] ring-1 ring-indigo-100/70 transition-transform transition-shadow duration-200 ease-out hover:-translate-y-0.5 hover:shadow-[0_16px_32px_rgba(15,23,42,0.18)] dark:border-indigo-500/20 dark:bg-gradient-to-br dark:from-slate-950/85 dark:via-slate-900/90 dark:to-indigo-950/80 dark:ring-white/10 dark:shadow-[0_18px_45px_rgba(0,0,0,0.45)] dark:hover:shadow-[0_22px_55px_rgba(0,0,0,0.6)]">
|
||
<div className="px-3 py-2.5 group-hover:animate-pulse">
|
||
<p className="mb-1.5 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-700 dark:text-indigo-100/80">
|
||
Admin Navigation
|
||
</p>
|
||
<div className="mb-2 h-px w-full bg-gradient-to-r from-transparent via-indigo-200/70 to-transparent opacity-80 transition-opacity group-hover:opacity-100 dark:via-indigo-200/40" />
|
||
<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-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
|
||
>
|
||
Dashboard
|
||
</button>
|
||
<button
|
||
onClick={() => { router.push('/admin/user-verify'); setMobileMenuOpen(false); }}
|
||
className="w-full text-left rounded-lg px-2 py-1.5 text-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
|
||
>
|
||
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-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
|
||
>
|
||
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-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
|
||
>
|
||
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-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
|
||
>
|
||
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-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
|
||
>
|
||
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-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
|
||
>
|
||
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-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
|
||
>
|
||
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-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
|
||
>
|
||
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-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
|
||
>
|
||
News Management
|
||
</button>
|
||
)}
|
||
|
||
{/* ADDED: Dev Management in hamburger admin nav */}
|
||
{isAdminOrSuper && (
|
||
<button
|
||
onClick={() => { router.push('/admin/dev-management'); setMobileMenuOpen(false); }}
|
||
className="w-full text-left rounded-lg px-2 py-1.5 text-slate-800 hover:bg-indigo-50 hover:text-slate-900 transition-colors dark:text-indigo-50 dark:hover:bg-white/10 dark:hover:text-white"
|
||
>
|
||
Dev 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>
|
||
)
|
||
} |