profit-planet-frontend/src/app/components/nav/Header.tsx
2025-10-16 10:09:31 +02:00

631 lines
29 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 } from 'react'
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import {
Dialog,
DialogPanel,
Disclosure,
DisclosureButton,
DisclosurePanel,
Popover,
PopoverButton,
PopoverGroup,
PopoverPanel,
Transition
} from '@headlessui/react'
import {
Bars3Icon,
ShoppingBagIcon,
UsersIcon,
UserCircleIcon,
XMarkIcon,
ArrowRightOnRectangleIcon,
MoonIcon,
SunIcon
} from '@heroicons/react/24/outline'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import useAuthStore from '../../store/authStore';
import { Avatar } from '../avatar';
import LanguageSwitcher from '../LanguageSwitcher';
// Replace current shopItems definition with detailed version (adds icon & description)
const shopItems = [
{ name: 'VIP', href: '/shop/vip', description: 'Exclusive VIP shop', icon: ShoppingBagIcon },
{ name: 'Public', href: '/shop/public', description: 'Open catalog for everyone', icon: UsersIcon },
];
const navLinks = [
{ name: 'Affiliate-Links', href: '/affiliate-links' },
{ name: 'Memberships', href: '/memberships' },
{ name: 'About us', href: '/about-us' },
];
export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [isDark, setIsDark] = useState(false)
const [mounted, setMounted] = useState(false) // added
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();
// NEW: permission flag
const [hasReferralPerm, setHasReferralPerm] = useState(false)
const handleLogout = async () => {
try {
await logout();
router.push('/login');
} catch (err) {
console.error('Logout failed:', err);
}
};
// 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';
};
// Theme initialization & persistence
useEffect(() => {
const stored = localStorage.getItem('theme')
if (stored === 'dark' || (!stored && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
setIsDark(true)
} else {
document.documentElement.classList.remove('dark')
setIsDark(false)
}
setMounted(true) // hydration complete
}, [])
const toggleTheme = useCallback(() => {
setIsDark(prev => {
const next = !prev
if (next) {
document.documentElement.classList.add('dark')
localStorage.setItem('theme', 'dark')
} else {
document.documentElement.classList.remove('dark')
localStorage.setItem('theme', 'light')
}
return next
})
}, [])
// 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])
const isLoggedIn = !!user
const userPresent = mounted && isLoggedIn
// NEW: detect admin role across common shapes
const isAdmin =
!!user &&
(
(user as any)?.role === 'admin' ||
(user as any)?.userType === 'admin' ||
(user as any)?.isAdmin === true ||
((user as any)?.roles?.includes?.('admin'))
)
return (
<header
// Remove bottom border when admin subheader is present to avoid a blue line under the gold bar
className={`relative isolate z-10 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'}`}
style={{
background: 'linear-gradient(135deg, #0F1D37 0%, #0A162A 50%, #081224 100%)',
}}
>
<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>
<div className="flex lg:hidden">
<button
type="button"
onClick={() => setMobileMenuOpen(true)}
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">Open main menu</span>
<Bars3Icon aria-hidden="true" className="size-6" />
</button>
</div>
<PopoverGroup className="hidden lg:flex lg:gap-x-12">
{/* Shop dropdown stays first */}
<Popover>
<PopoverButton className="flex items-center gap-x-1 text-sm/6 font-semibold text-gray-900 dark:text-white">
Shop
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none text-gray-500" />
</PopoverButton>
{/* ...existing Shop PopoverPanel... */}
<PopoverPanel
transition
className="absolute left-0 right-0 top-full z-50 rounded-b-2xl shadow-xl shadow-black/40 ring-1 ring-white/10 dark:ring-white/15 overflow-hidden data-closed:-translate-y-1 data-closed:opacity-0 data-enter:duration-200 data-enter:ease-out data-leave:duration-150 data-leave:ease-in"
style={{
background: 'linear-gradient(150deg, rgba(26,46,84,0.95) 0%, rgba(18,37,70,0.92) 45%, rgba(30,56,104,0.88) 100%)',
backdropFilter: 'blur(26px) saturate(175%)',
WebkitBackdropFilter: 'blur(26px) saturate(175%)'
}}
>
<div className="relative before:absolute before:inset-0 before:pointer-events-none before:bg-[radial-gradient(circle_at_18%_30%,rgba(56,124,255,0.30),transparent_62%),radial-gradient(circle_at_82%_40%,rgba(139,92,246,0.22),transparent_65%)]">
<div className="mx-auto grid max-w-7xl grid-cols-2 md:grid-cols-3 gap-x-4 px-6 py-10 lg:px-8 xl:gap-x-8">
{shopItems.map(item => (
<div
key={item.name}
className="group relative rounded-lg p-6 text-sm/6 hover:bg-white/5 transition-colors"
>
<div className="flex size-11 items-center justify-center rounded-lg bg-white/10 backdrop-blur-md group-hover:bg-white/20 transition-colors">
<item.icon aria-hidden="true" className="size-6 text-gray-300 group-hover:text-white" />
</div>
<button
onClick={() => router.push(item.href)}
className="mt-6 block font-semibold text-white"
>
{item.name}
<span className="absolute inset-0" />
</button>
<p className="mt-1 text-gray-300">{item.description}</p>
</div>
))}
</div>
</div>
</PopoverPanel>
</Popover>
{/* Affiliate Links */}
<button
onClick={() => router.push('/affiliate-links')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
Affiliate-Links
</button>
{/* Memberships */}
<button
onClick={() => router.push('/memberships')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
Memberships
</button>
{/* Referral Management - match others (no highlight) */}
{userPresent && hasReferralPerm && (
<button
onClick={() => { console.log('🧭 Header: navigate to /referral-management'); router.push('/referral-management') }}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
Referral Management
</button>
)}
{/* About us */}
<button
onClick={() => router.push('/about-us')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
About us
</button>
</PopoverGroup>
<div className="hidden lg:flex lg:flex-1 lg:justify-end lg:items-center lg:gap-x-4">
{/* Stable auth slot to avoid SSR/CSR structural drift */}
<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>
<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>
<button
onClick={handleLogout}
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"
>
<ArrowRightOnRectangleIcon className="size-5 text-gray-400" />
Logout
</button>
</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>
{/* Language & theme remain after auth slot */}
<LanguageSwitcher variant={isDark ? 'dark' : 'light'} />
<button
onClick={toggleTheme}
aria-label="Toggle theme"
className="p-2 rounded-md border border-gray-300 dark:border-white/10 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors"
>
{isDark ? <SunIcon className="h-5 w-5" /> : <MoonIcon className="h-5 w-5" />}
</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={() => { console.log('🧭 Admin: navigate to /admin'); router.push('/admin') }}
className="text-sm font-semibold text-[#0F1D37] hover:text-[#7A5E1A]"
>
Dashboard
</button>
<button
onClick={() => { console.log('🧭 Admin: navigate to /admin/user-management'); router.push('/admin/user-management') }}
className="text-sm font-semibold text-[#0F1D37] hover:text-[#7A5E1A]"
>
User Management
</button>
<button
onClick={() => { console.log('🧭 Admin: navigate to /admin/user-verify'); router.push('/admin/user-verify') }}
className="text-sm font-semibold text-[#0F1D37] hover:text-[#7A5E1A]"
>
User Verify
</button>
{/* NEW: Matrix Management link */}
<button
onClick={() => { console.log('🧭 Admin: navigate to /admin/matrix-management'); router.push('/admin/matrix-management') }}
className="text-sm font-semibold text-[#0F1D37] hover:text-[#7A5E1A]"
>
Matrix Management
</button>
</div>
</div>
</div>
)}
{/* Mobile dialog and rest of header */}
<Dialog open={mobileMenuOpen} onClose={setMobileMenuOpen} className="lg:hidden">
<Transition
appear
show={mobileMenuOpen}
>
<Transition.Child
enter="transition-opacity duration-300 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-200 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-40 bg-black/40 backdrop-blur-sm" />
</Transition.Child>
{/* Sliding panel */}
<Transition.Child
enter="transform transition-transform duration-300 ease-out"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition-transform duration-250 ease-in"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<DialogPanel className="fixed inset-y-0 right-0 z-50 w-full sm:max-w-sm h-full overflow-y-auto overflow-x-hidden bg-white dark:bg-gray-900 p-5 sm:ring-1 sm:ring-black/10 dark:sm:ring-gray-100/10">
<div className="flex items-center justify-between">
<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-600 dark:hover:text-gray-200 transition-transform duration-300 hover:scale-110"
>
<span className="sr-only">Close menu</span>
<XMarkIcon aria-hidden="true" className="size-6" />
</button>
</div>
<div className="mt-6 flow-root">
<div className="-my-6 divide-y divide-gray-200 dark:divide-white/10">
{!mounted ? (
<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 now FIRST under logo */}
<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>
<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>
<button
onClick={() => { handleLogout(); 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"
>
Logout
</button>
</div>
{/* Theme + Language now AFTER user info */}
<div className="py-6">
<button
onClick={toggleTheme}
className="flex items-center gap-x-2 rounded-lg px-3 py-2.5 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-white/5 w-full"
>
{isDark ? <SunIcon className="h-5 w-5" /> : <MoonIcon className="h-5 w-5" />}
{isDark ? 'Light Mode' : 'Dark Mode'}
</button>
<div className="mt-4 px-1">
<LanguageSwitcher variant="dark" />
</div>
</div>
{/* Navigation / Shop after that */}
<div className="space-y-2 py-6">
<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">
Shop
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none group-data-open:rotate-180" />
</DisclosureButton>
<DisclosurePanel className="mt-2 space-y-1">
{shopItems.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>
{/* Affiliate Links */}
<button
onClick={() => { router.push('/affiliate-links'); 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"
>
Affiliate-Links
</button>
{/* Memberships */}
<button
onClick={() => { router.push('/memberships'); 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"
>
Memberships
</button>
{/* Referral Management - match others (no highlight) */}
{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>
)}
{/* About us */}
<button
onClick={() => { router.push('/about-us'); 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"
>
About us
</button>
</div>
</>
) : (
<div className="py-6 space-y-4">
<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">
Shop
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none group-data-open:rotate-180" />
</DisclosureButton>
<DisclosurePanel className="mt-2 space-y-1">
{shopItems.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>
<button
onClick={() => { router.push('/affiliate-links'); 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"
>
Affiliate-Links
</button>
<button
onClick={() => { router.push('/memberships'); 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"
>
Memberships
</button>
<button
onClick={() => { router.push('/about-us'); 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"
>
About us
</button>
<div className="px-3">
<button
onClick={() => { router.push('/login'); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
Log in
</button>
</div>
</div>
)}
</div>
</div>
</DialogPanel>
</Transition.Child>
</Transition>
</Dialog>
</header>
)
}