You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

473 lines
20 KiB
TypeScript

'use client';
import { useState, useEffect, useMemo, useRef } from 'react';
import useTranslation from '../hooks/useTranslation';
import Link from 'next/link';
import { Menu, X, BarChart3, LogOut, Zap, Users, Settings, Calendar, User, HelpCircle, Globe, ChevronDown } from 'lucide-react';
import { useUserSettings } from '@/src/contexts/UserSettingsContext';
import { motion, AnimatePresence } from 'framer-motion';
import { useSwissOIDAuth } from 'swissoid-front';
import PartnerSearchModal from './PartnerSearchModal';
import ClaimCredentialsModal from './ClaimCredentialsModal';
import { getClaimedRemoteMember } from '@/src/utils/hasClaimedAnyCredentials';
import { useLocalizedLink } from '@/src/hooks/useLocalizedLink';
import { cleanName } from '@/src/utils/bookingUtils';
import { useRouter, usePathname } from 'next/navigation';
import { i18n } from '@/i18n-config';
interface NavigationProps {
pageTitle?: string;
}
export default function Navigation({ pageTitle }: NavigationProps) {
const { user, login, logout } = useSwissOIDAuth();
const isLoggedIn = !!user;
const { t } = useTranslation();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { settings, appUser, remoteMembers } = useUserSettings();
const [showPartnerModal, setShowPartnerModal] = useState(false);
const [showClaimModal, setShowClaimModal] = useState(false);
const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = useState(false);
const { locale, localizedLink } = useLocalizedLink();
const router = useRouter();
const pathname = usePathname();
const menuScrollContainerRef = useRef<HTMLDivElement>(null);
// Get the current user's name from remote members
const currentUserData = useMemo(() => {
if (!settings || !remoteMembers) return null;
const claimedMember = getClaimedRemoteMember(settings);
if (!claimedMember) return null;
const member = remoteMembers.find(m => m.remote_member_id === claimedMember.remote_member_id);
if (!member) return null;
return {
name: cleanName(member.full_account_str),
initials: cleanName(member.full_account_str).split(' ').map(n => n[0]?.toUpperCase()).filter(Boolean).slice(0, 2).join('')
};
}, [settings, remoteMembers]);
const toggleMenu = () => setIsMenuOpen(!isMenuOpen);
const closeMenu = () => {
setIsMenuOpen(false);
// Multiple fallbacks to ensure menu closes even on fast navigation
setTimeout(() => setIsMenuOpen(false), 0);
setTimeout(() => setIsMenuOpen(false), 50);
setTimeout(() => setIsMenuOpen(false), 100);
};
// Language options
const languages = [
{ code: 'en-US', label: 'English', flag: '🇺🇸' },
{ code: 'de-DE', label: 'Deutsch', flag: '🇩🇪' },
{ code: 'fr-CH', label: 'Français', flag: '🇨🇭' }
];
// Handle language change
const handleLanguageChange = (newLocale: string) => {
// Get the current pathname without the locale
const segments = pathname.split('/');
const currentLocale = segments[1];
// Check if the first segment is a locale
if (i18n.locales.includes(currentLocale as any)) {
segments[1] = newLocale;
} else {
// If no locale in path, add it
segments.unshift('', newLocale);
}
const newPath = segments.join('/').replace(/\/+/g, '/');
router.push(newPath);
};
// Handle language dropdown toggle with scroll
const handleLanguageDropdownToggle = () => {
const newState = !isLanguageDropdownOpen;
setIsLanguageDropdownOpen(newState);
// If opening the dropdown, scroll to bottom of menu
if (newState && menuScrollContainerRef.current) {
setTimeout(() => {
const container = menuScrollContainerRef.current;
if (container) {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
});
}
}, 300);
}
};
// Prevent body scrolling when menu is open
useEffect(() => {
if (isMenuOpen) {
// Save current scroll position
const scrollY = window.scrollY;
// Add styles to prevent scrolling
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollY}px`;
document.body.style.width = '100%';
document.body.style.overflow = 'hidden';
return () => {
// Restore scroll position when menu closes
document.body.style.position = '';
document.body.style.top = '';
document.body.style.width = '';
document.body.style.overflow = '';
window.scrollTo(0, scrollY);
};
}
}, [isMenuOpen]);
const menuVariants = {
closed: {
x: '100%',
transition: {
type: 'spring' as const,
stiffness: 400,
damping: 40
}
},
open: {
x: 0,
transition: {
type: 'spring' as const,
stiffness: 400,
damping: 40
}
}
};
const overlayVariants = {
closed: { opacity: 0 },
open: { opacity: 1 }
};
const menuItemVariants = {
closed: { x: 50, opacity: 0 },
open: (i: number) => ({
x: 0,
opacity: 1,
transition: {
delay: i * 0.1,
type: 'spring' as const,
stiffness: 400,
damping: 25
}
})
};
const bookingLink = localizedLink(
settings?.default_remote_sport.facility_slug && settings?.default_remote_sport.sport_slug
? `/booking/${encodeURIComponent(settings.default_remote_sport.facility_slug)}/${encodeURIComponent(settings.default_remote_sport.sport_slug)}/today`
: '/select-remote'
);
return (
<>
<header className="bg-white shadow-sm border-b border-slate-200 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<div className="flex items-center">
<Link href={localizedLink("/")} className="flex items-center group">
<div className="relative">
<h1 className="text-2xl font-bold text-slate-900">
Playchoo
</h1>
<div className="absolute -bottom-1 left-0 w-0 h-0.5 bg-slate-900 group-hover:w-full transition-all duration-300"></div>
</div>
</Link>
{pageTitle && (
<span className="text-xl text-slate-600 font-normal ml-3 pl-3 border-l-2 border-slate-300">{pageTitle}</span>
)}
</div>
{/* Right side navigation */}
<div className="flex items-center space-x-4">
{/* Always show hamburger on mobile, show login/register on desktop when logged out */}
<div className="hidden sm:flex items-center space-x-3">
{!isLoggedIn && (
<>
<button
onClick={() => login()}
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-slate-900 transition-colors duration-200"
>
{t('Login')}
</button>
<button
onClick={() => login()}
className="px-4 py-2 text-sm font-medium bg-slate-900 text-white rounded-lg hover:bg-slate-800 transform hover:scale-105 transition-all duration-200 shadow-lg hover:shadow-xl"
>
{t('Register')}
</button>
</>
)}
</div>
{/* Hamburger menu button - always visible on mobile, only for logged-in users on desktop */}
<button
onClick={toggleMenu}
className={`relative p-2 rounded-xl bg-slate-900 text-white shadow-lg hover:shadow-xl hover:bg-slate-800 transition-all duration-200 group ${!isLoggedIn ? 'block sm:hidden' : 'block'}`}
>
<motion.div
animate={{ rotate: isMenuOpen ? 180 : 0 }}
transition={{ duration: 0.3 }}
>
{isMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</motion.div>
</button>
</div>
</div>
</div>
</header>
{/* Mobile/Desktop Side Menu */}
<AnimatePresence>
{isMenuOpen && (
<>
{/* Overlay */}
<motion.div
variants={overlayVariants}
initial="closed"
animate="open"
exit="closed"
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-40"
onClick={closeMenu}
/>
{/* Side Menu */}
<motion.div
variants={menuVariants}
initial="closed"
animate="open"
exit="closed"
className="fixed top-0 right-0 h-full w-full sm:w-96 bg-white shadow-2xl z-50 flex flex-col"
>
<div className="flex flex-col h-full overflow-hidden">
{/* Header */}
<div className="bg-white border-b border-slate-200 p-6 flex-shrink-0">
<div className="flex items-center justify-between">
{isLoggedIn && appUser && currentUserData ? (
<Link
href={localizedLink(`/profile/user/${appUser.app_user_id}/${settings?.default_remote_sport.sport_slug || ''}`)}
onClick={closeMenu}
className="flex items-center space-x-3 group"
>
<div className="w-10 h-10 rounded-full bg-slate-900 flex items-center justify-center text-white font-semibold text-sm">
{currentUserData.initials}
</div>
<div>
<div className="font-medium text-slate-900 group-hover:text-slate-700 transition-colors">
{currentUserData.name}
</div>
<div className="text-xs text-slate-500">{t('View Profile')}</div>
</div>
</Link>
) : (
<div>
<h2 className="text-xl font-bold text-slate-900">{t('Menu')}</h2>
<p className="text-slate-500 text-sm">{t('Navigate your game')}</p>
</div>
)}
<button
onClick={closeMenu}
className="p-2 pl-6 rounded-lg hover:bg-slate-50 transition-colors duration-200 text-slate-600"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* Menu Items */}
<div ref={menuScrollContainerRef} className="flex-1 p-6 space-y-2 overflow-y-auto">
{!isLoggedIn ? (
<>
<motion.div
custom={0}
variants={menuItemVariants}
initial="closed"
animate="open"
>
<button
onClick={() => { login(); closeMenu(); }}
className="flex items-center space-x-3 p-4 rounded-xl hover:bg-slate-50 transition-colors duration-200 group w-full"
>
<div className="p-2 rounded-lg bg-slate-100 group-hover:bg-slate-200 transition-colors duration-200">
<User className="w-5 h-5 text-slate-700" />
</div>
<span className="font-medium text-slate-900">{t('Login')}</span>
</button>
</motion.div>
<motion.div
custom={1}
variants={menuItemVariants}
initial="closed"
animate="open"
>
<button
onClick={() => { login(); closeMenu(); }}
className="flex items-center space-x-3 p-4 rounded-xl hover:bg-slate-50 transition-colors duration-200 group w-full"
>
<div className="p-2 rounded-lg bg-slate-100 group-hover:bg-slate-200 transition-colors duration-200">
<Users className="w-5 h-5 text-slate-700" />
</div>
<span className="font-medium text-slate-900">{t('Register')}</span>
</button>
</motion.div>
</>
) : (
<>
<motion.div
custom={0}
variants={menuItemVariants}
initial="closed"
animate="open"
>
<Link
href={localizedLink("/admin/facilities")}
onClick={closeMenu}
className="flex items-center space-x-3 p-4 rounded-xl hover:bg-slate-50 transition-colors duration-200 group"
>
<div className="p-2 rounded-lg bg-purple-100 group-hover:bg-purple-200 transition-colors duration-200">
<Settings className="w-5 h-5 text-purple-700" />
</div>
<span className="font-medium text-slate-900">{t('Venue Management')}</span>
</Link>
</motion.div>
</>
)}
{/* Language Selector - Available for all users */}
<div className="my-4 border-t border-slate-100"></div>
<motion.div
custom={isLoggedIn ? 1 : 2}
variants={menuItemVariants}
initial="closed"
animate="open"
>
<div className="px-4 py-2">
{/* Language Dropdown Header */}
<button
onClick={handleLanguageDropdownToggle}
className="w-full flex items-center justify-between p-3 rounded-lg hover:bg-slate-50 transition-colors duration-200"
>
<div className="flex items-center space-x-3">
<Globe className="w-4 h-4 text-slate-600" />
<span className="font-medium text-slate-700">{t('Language')}</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-xl">{languages.find(l => l.code === locale)?.flag}</span>
<span className="font-medium text-slate-700">{languages.find(l => l.code === locale)?.label}</span>
<ChevronDown className={`w-4 h-4 text-slate-500 transition-transform duration-200 ${isLanguageDropdownOpen ? 'rotate-180' : ''}`} />
</div>
</button>
{/* Language Dropdown Options */}
<AnimatePresence>
{isLanguageDropdownOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="mt-2 space-y-1 pl-7">
{languages.filter(lang => lang.code !== locale).map((lang) => (
<button
key={lang.code}
onClick={() => {
handleLanguageChange(lang.code);
setIsLanguageDropdownOpen(false);
closeMenu();
}}
className="w-full flex items-center space-x-3 px-3 py-2 rounded-lg hover:bg-slate-100 text-slate-700 transition-colors duration-200"
>
<span className="text-xl">{lang.flag}</span>
<span className="font-medium">{lang.label}</span>
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
</div>
{/* Footer */}
<div className="border-t-2 border-slate-200 p-6 space-y-4 flex-shrink-0">
{isLoggedIn && (
<div className="flex items-center gap-3">
<motion.div
custom={2}
variants={menuItemVariants}
initial="closed"
animate="open"
className="flex-1"
>
<Link
href={localizedLink("/logout")}
onClick={closeMenu}
className="flex items-center space-x-3 p-4 rounded-xl hover:bg-red-50 transition-colors duration-200 group text-red-600"
>
<div className="p-2 rounded-lg bg-red-100 text-red-600 group-hover:bg-red-200 transition-colors duration-200">
<LogOut className="w-5 h-5" />
</div>
<span className="font-medium">{t('Logout')}</span>
</Link>
</motion.div>
<motion.div
custom={3}
variants={menuItemVariants}
initial="closed"
animate="open"
>
<Link
href={localizedLink("/settings")}
onClick={closeMenu}
className="flex items-center justify-center w-12 h-12 rounded-lg bg-slate-100 hover:bg-slate-200 transition-colors duration-200"
title={t('Settings')}
>
<Settings className="w-5 h-5 text-slate-600" />
</Link>
</motion.div>
</div>
)}
<div className="flex items-center justify-between text-sm text-slate-500 pt-4 border-t border-slate-100">
<span>Version {process.env.NEXT_PUBLIC_APP_VERSION}</span>
<div className="flex items-center space-x-1">
<Zap className="w-4 h-4 text-slate-400" />
<span>{t('Powered by passion')}</span>
</div>
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
<PartnerSearchModal
isOpen={showPartnerModal}
onClose={() => setShowPartnerModal(false)}
onSelectPartner={() => {}}
remoteSlug={settings?.default_remote_sport.facility_slug || ''}
sportSlug={settings?.default_remote_sport.sport_slug || ''}
selectedPartners={[]}
context="lookup"
/>
<ClaimCredentialsModal
isOpen={showClaimModal}
onClose={() => setShowClaimModal(false)}
/>
</>
);
}