851 lines
30 KiB
TypeScript
851 lines
30 KiB
TypeScript
import { useState, useEffect, useRef, forwardRef } from 'react';
|
|
import { Menu, X, ShoppingBag, ChevronDown, Globe, User, Settings, LogOut } from 'lucide-react';
|
|
import { motion, AnimatePresence } from 'motion/react';
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
import Frame1597884853 from '../imports/Frame1597884853';
|
|
import { Button } from './ui/button';
|
|
import { ImageWithFallback } from './figma/ImageWithFallback';
|
|
import { CTAButton } from './CTAButton';
|
|
import logoImage from '../assets/cit-logo.png';
|
|
import melbourneLogo from '../assets/melbourne-logo.png';
|
|
import { CitySelectionDialog } from './CitySelectionDialog';
|
|
|
|
interface NavbarProps {
|
|
activeCity: string;
|
|
onCityChange: (city: string) => void;
|
|
onSignInClick: () => void;
|
|
onSignOutClick?: () => void;
|
|
isUserSignedIn?: boolean;
|
|
user?: { email: string; name: string } | null;
|
|
}
|
|
|
|
interface DropdownItem {
|
|
id: string;
|
|
label: string;
|
|
icon?: React.ReactNode;
|
|
action?: () => void;
|
|
badge?: string | number;
|
|
path?: string;
|
|
}
|
|
|
|
interface CartItem {
|
|
id: string;
|
|
name: string;
|
|
price: string;
|
|
image?: string;
|
|
quantity: number;
|
|
}
|
|
|
|
interface DropdownProps {
|
|
isOpen: boolean;
|
|
onToggle: () => void;
|
|
items: DropdownItem[];
|
|
trigger: React.ReactNode;
|
|
title?: string;
|
|
className?: string;
|
|
}
|
|
|
|
interface NavigationItem {
|
|
id: number;
|
|
label: string;
|
|
path: string;
|
|
displayLabel: string;
|
|
source: 'landing' | 'melbourne';
|
|
position: number;
|
|
isShared?: boolean;
|
|
landingLabel?: string;
|
|
melbourneLabel?: string;
|
|
}
|
|
|
|
export default function Navbar({
|
|
activeCity,
|
|
onCityChange,
|
|
onSignInClick,
|
|
onSignOutClick,
|
|
isUserSignedIn = false,
|
|
user
|
|
}: NavbarProps) {
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
const [isScrolled, setIsScrolled] = useState(false);
|
|
const [activeLanguageDropdown, setActiveLanguageDropdown] = useState(false);
|
|
const [activeCartDropdown, setActiveCartDropdown] = useState(false);
|
|
const [activeUserDropdown, setActiveUserDropdown] = useState(false);
|
|
const [activeCityDropdown, setActiveCityDropdown] = useState(false);
|
|
const [isCityDialogOpen, setIsCityDialogOpen] = useState(false);
|
|
const [navigationSource, setNavigationSource] = useState<'landing' | 'melbourne'>('landing');
|
|
|
|
const [dialogSource, setDialogSource] = useState<'navbar' | 'cta'>('navbar');
|
|
|
|
const languageRef = useRef<HTMLDivElement>(null);
|
|
const cartRef = useRef<HTMLDivElement>(null);
|
|
const userRef = useRef<HTMLDivElement>(null);
|
|
const cityRef = useRef<HTMLDivElement>(null);
|
|
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
|
|
// More flexible navigation configuration
|
|
const navigationConfig = {
|
|
landing: [
|
|
// Position 1 - Shared item
|
|
{
|
|
label: 'Discover',
|
|
path: '/how-it-works',
|
|
isShared: true,
|
|
landingLabel: 'Discover',
|
|
melbourneLabel: 'How It Works'
|
|
},
|
|
// Position 2
|
|
{
|
|
label: 'Magic Itinerary',
|
|
path: '/landing-magic-itinerary',
|
|
isShared: false
|
|
},
|
|
// Position 3
|
|
{
|
|
label: 'Whats Included',
|
|
path: '/whats-included',
|
|
isShared: false
|
|
},
|
|
// Position 4 - Shared item
|
|
{
|
|
label: 'Your Card',
|
|
path: '/passes',
|
|
isShared: true,
|
|
landingLabel: 'Your Card',
|
|
melbourneLabel: 'Your Card'
|
|
},
|
|
// Position 5
|
|
{
|
|
label: 'FAQ',
|
|
path: '/faq',
|
|
isShared: false
|
|
},
|
|
],
|
|
melbourne: [
|
|
// Position 1
|
|
{
|
|
label: 'Attractions',
|
|
path: '/attractions',
|
|
isShared: false
|
|
},
|
|
// Position 2
|
|
{
|
|
label: 'Magic Itinerary',
|
|
path: '/magic-itinerary',
|
|
isShared: false
|
|
},
|
|
// Position 3
|
|
{
|
|
label: 'Super Savings',
|
|
path: '/super-savings',
|
|
isShared: false
|
|
},
|
|
// Position 4 - Shared item
|
|
{
|
|
label: 'How It Works',
|
|
path: '/how-it-works',
|
|
isShared: true,
|
|
landingLabel: 'Discover',
|
|
melbourneLabel: 'How It Works'
|
|
},
|
|
// Position 5 - Shared item
|
|
{
|
|
label: 'Your Card',
|
|
path: '/passes',
|
|
isShared: true,
|
|
landingLabel: 'Your Card',
|
|
melbourneLabel: 'Your Card'
|
|
}
|
|
]
|
|
};
|
|
|
|
// Check if we're on landing page
|
|
const isLandingPage = location.pathname === '/';
|
|
|
|
// Auto-detect navigation source based on activeCity and current page
|
|
const getAutoNavigationSource = (): 'landing' | 'melbourne' => {
|
|
// If activeCity is explicitly set to 'shared', detect from context
|
|
if (activeCity.toLowerCase() === 'shared') {
|
|
// Check if we're on Melbourne-specific pages
|
|
const isMelbournePage =
|
|
location.pathname === '/melbourne' ||
|
|
location.pathname.startsWith('/attractions') ||
|
|
location.pathname === '/magic-itinerary' ||
|
|
location.pathname === '/super-savings';
|
|
|
|
return isMelbournePage ? 'melbourne' : 'landing';
|
|
}
|
|
|
|
// If activeCity is explicitly Melbourne, use melbourne source
|
|
if (activeCity.toLowerCase() === 'melbourne') {
|
|
return 'melbourne';
|
|
}
|
|
|
|
// Default to landing
|
|
return 'landing';
|
|
};
|
|
|
|
// Get navigation items based on current context
|
|
const getNavigationItems = (): NavigationItem[] => {
|
|
const currentSource = getAutoNavigationSource();
|
|
const items = currentSource === 'landing' ?
|
|
navigationConfig.landing : navigationConfig.melbourne;
|
|
|
|
return items.map((item, index) => ({
|
|
...item,
|
|
id: index + 1,
|
|
displayLabel: currentSource === 'landing'
|
|
? (item.landingLabel || item.label)
|
|
: (item.melbourneLabel || item.label),
|
|
source: currentSource,
|
|
position: index + 1
|
|
}));
|
|
};
|
|
|
|
const navigationItems = getNavigationItems();
|
|
|
|
// Enhanced active state logic with proper typing
|
|
const isNavItemActive = (item: NavigationItem): boolean => {
|
|
const currentPath = location.pathname;
|
|
const currentSource = getAutoNavigationSource();
|
|
|
|
// Special handling for shared paths
|
|
if (item.isShared) {
|
|
return currentPath === item.path && currentSource === item.source;
|
|
}
|
|
|
|
// Default active state for other paths
|
|
return currentPath === item.path;
|
|
};
|
|
|
|
const handleOpenCityDialogFromNavbar = () => {
|
|
setDialogSource('navbar');
|
|
setIsCityDialogOpen(true);
|
|
};
|
|
|
|
const handleOpenCityDialogFromCTA = () => {
|
|
setDialogSource('cta');
|
|
setIsCityDialogOpen(true);
|
|
};
|
|
|
|
const handleCloseCityDialog = () => {
|
|
setIsCityDialogOpen(false);
|
|
setDialogSource('navbar');
|
|
};
|
|
|
|
const handleCitySelectFromNavbar = (cityId: string) => {
|
|
console.log('City selected from navbar:', cityId);
|
|
onCityChange(cityId);
|
|
|
|
if (cityId.toLowerCase() === 'melbourne') {
|
|
setNavigationSource('melbourne');
|
|
navigate('/melbourne');
|
|
} else {
|
|
setNavigationSource('landing');
|
|
navigate('/passes');
|
|
}
|
|
|
|
handleCloseCityDialog();
|
|
};
|
|
|
|
const handleCitySelectFromCTA = (cityId: string) => {
|
|
console.log('City selected from CTA:', cityId);
|
|
onCityChange(cityId);
|
|
|
|
if (!isUserSignedIn) {
|
|
setNavigationSource('landing');
|
|
navigate('/passes');
|
|
} else {
|
|
if (cityId.toLowerCase() === 'melbourne') {
|
|
setNavigationSource('melbourne');
|
|
navigate('/melbourne');
|
|
} else {
|
|
setNavigationSource('landing');
|
|
navigate('/passes');
|
|
}
|
|
}
|
|
|
|
handleCloseCityDialog();
|
|
};
|
|
|
|
const handleCitySelect = (cityId: string) => {
|
|
if (dialogSource === 'cta') {
|
|
handleCitySelectFromCTA(cityId);
|
|
} else {
|
|
handleCitySelectFromNavbar(cityId);
|
|
}
|
|
};
|
|
|
|
// Available cities for mobile dropdown
|
|
const cities = [
|
|
{ id: 'melbourne', label: 'Melbourne' },
|
|
{ id: 'sydney', label: 'Sydney' },
|
|
{ id: 'brisbane', label: 'Brisbane' },
|
|
{ id: 'perth', label: 'Perth' }
|
|
];
|
|
|
|
// Languages available
|
|
const languages: DropdownItem[] = [
|
|
{ id: 'en', label: 'English', icon: <span className="text-base">🇺🇸</span> },
|
|
{ id: 'es', label: 'Español', icon: <span className="text-base">🇪🇸</span> },
|
|
{ id: 'fr', label: 'Français', icon: <span className="text-base">🇫🇷</span> },
|
|
{ id: 'it', label: 'Italiano', icon: <span className="text-base">🇮🇹</span> },
|
|
];
|
|
|
|
// Mock cart items
|
|
const cartItems: CartItem[] = [
|
|
{ id: '1', name: 'Sydney 2-Day Pass', price: '$89', quantity: 1 },
|
|
{ id: '2', name: 'Melbourne Premium Pass', price: '$129', quantity: 1 },
|
|
];
|
|
|
|
// Calculate cart total
|
|
const cartTotal = cartItems.reduce((total, item) => {
|
|
const price = parseFloat(item.price.replace('$', ''));
|
|
return total + (price * item.quantity);
|
|
}, 0);
|
|
|
|
// Cart dropdown items with proper navigation for checkout
|
|
const cartDropdownItems: DropdownItem[] = [
|
|
...cartItems.map(item => ({
|
|
id: item.id,
|
|
label: `${item.name} - ${item.price}`,
|
|
badge: `${item.quantity}x`
|
|
})),
|
|
{
|
|
id: 'total',
|
|
label: `Total: $${cartTotal.toFixed(2)}`,
|
|
icon: <ShoppingBag className="w-4 h-4" />
|
|
},
|
|
{
|
|
id: 'checkout',
|
|
label: 'Proceed to Checkout',
|
|
action: () => {
|
|
navigate('/checkout');
|
|
setActiveCartDropdown(false);
|
|
}
|
|
}
|
|
];
|
|
|
|
const closeMobileMenu = () => {
|
|
setIsMobileMenuOpen(false);
|
|
};
|
|
|
|
// Enhanced navigation handler with proper typing
|
|
const handleNavClick = (path: string) => {
|
|
navigate(path);
|
|
closeMobileMenu();
|
|
};
|
|
|
|
// Get navigation source for an item with proper typing
|
|
const getNavigationSource = (item: NavigationItem): 'landing' | 'melbourne' => {
|
|
return item.source;
|
|
};
|
|
|
|
// Detect scroll for navbar styling
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
const scrolled = window.scrollY > 20;
|
|
setIsScrolled(scrolled);
|
|
};
|
|
|
|
window.addEventListener('scroll', handleScroll);
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, []);
|
|
|
|
// Handle city change for mobile dropdown
|
|
const handleMobileCityChange = (city: string) => {
|
|
console.log('City selected:', city);
|
|
onCityChange(city);
|
|
setActiveCityDropdown(false);
|
|
|
|
if (city.toLowerCase() === 'melbourne') {
|
|
setNavigationSource('melbourne');
|
|
navigate('/melbourne');
|
|
} else {
|
|
setNavigationSource('landing');
|
|
navigate('/comming-soon');
|
|
}
|
|
};
|
|
|
|
// Simple Dropdown component without blinking
|
|
const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(({
|
|
isOpen,
|
|
onToggle,
|
|
items,
|
|
trigger,
|
|
title,
|
|
className = ""
|
|
}, ref) => (
|
|
<div ref={ref} className={`relative ${className}`}>
|
|
<div
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggle();
|
|
}}
|
|
className="cursor-pointer"
|
|
>
|
|
{trigger}
|
|
</div>
|
|
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<>
|
|
{/* Backdrop to capture outside clicks */}
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-40"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggle();
|
|
}}
|
|
/>
|
|
|
|
{/* Dropdown content */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}
|
|
className="absolute top-full left-0 mt-2 bg-white/95 backdrop-blur-xl rounded-2xl shadow-xl border border-white/20 min-w-[200px] overflow-hidden z-50"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{title && (
|
|
<div className="px-5 py-4 border-b border-gray-100/50">
|
|
<h3 className="font-merchant font-semibold text-gray-900 text-base">{title}</h3>
|
|
</div>
|
|
)}
|
|
|
|
<div className="py-2">
|
|
{items.map((item, index) => (
|
|
<div
|
|
key={item.id}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
console.log('Dropdown item clicked:', item.label);
|
|
|
|
if (item.action) {
|
|
item.action();
|
|
}
|
|
}}
|
|
className="px-4 py-2.5 hover:bg-gray-50/80 cursor-pointer transition-colors duration-200"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium text-gray-700">{item.label}</span>
|
|
{item.badge && (
|
|
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full">
|
|
{item.badge}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
</>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
));
|
|
|
|
// Set display name for debugging
|
|
Dropdown.displayName = 'Dropdown';
|
|
|
|
// Get current navigation source for display
|
|
const currentSource = getAutoNavigationSource();
|
|
|
|
return (
|
|
<>
|
|
{/* Desktop Navbar - Enhanced Glassmorphism */}
|
|
<motion.nav
|
|
className="fixed top-0 left-0 right-0 z-50 hidden lg:block"
|
|
initial={{ y: -100, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
transition={{ duration: 0.6, ease: [0.25, 0.1, 0.25, 1], delay: 0.2 }}
|
|
>
|
|
<div className="">
|
|
<motion.div
|
|
className={`w-full transition-all duration-500 ease-out px-8 py-4 bg-white backdrop-blur-[20px] border border-white/20 ${isScrolled
|
|
? 'shadow-[0_10px_15px_-3px_rgba(0,0,0,0.08),0_4px_6px_-2px_rgba(0,0,0,0.05)]'
|
|
: 'shadow-lg shadow-black/5'
|
|
}`}
|
|
initial={{ scale: 0.95, opacity: 0, y: 0 }}
|
|
animate={{
|
|
scale: isScrolled ? 0.98 : 1,
|
|
opacity: 1,
|
|
y: isScrolled ? 2 : 0
|
|
}}
|
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
{/* Logo */}
|
|
<motion.div
|
|
className="flex items-center cursor-pointer flex-shrink-0"
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
>
|
|
<Link to="/">
|
|
<ImageWithFallback
|
|
src={currentSource === 'melbourne' ? melbourneLogo : logoImage}
|
|
alt={
|
|
currentSource === 'melbourne'
|
|
? 'Melbourne CityCards Logo'
|
|
: 'CityCards Logo'
|
|
}
|
|
className="h-10 w-auto"
|
|
/>
|
|
</Link>
|
|
</motion.div>
|
|
|
|
<div className="absolute -translate-x-1/2 flex items-center gap-5"
|
|
style={{ left: '45%', }}
|
|
>
|
|
{/* Enhanced Navigation Items with source tracking */}
|
|
{navigationItems.map((item) => {
|
|
const source = getNavigationSource(item);
|
|
|
|
return (
|
|
<div
|
|
key={`${item.path}-${source}-${item.id}`}
|
|
onClick={() => handleNavClick(item.path)}
|
|
className={`relative px-0 py-2 text-base font-medium transition-all duration-200 whitespace-nowrap group cursor-pointer ${isNavItemActive(item)
|
|
? 'text-primary'
|
|
: 'text-gray-700 hover:text-gray-900'
|
|
}`}
|
|
>
|
|
{item.displayLabel}
|
|
{/* Active indicator */}
|
|
<motion.div
|
|
className="absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-primary to-secondary rounded-full"
|
|
initial={{ width: 0 }}
|
|
animate={{
|
|
width: isNavItemActive(item) ? "100%" : 0,
|
|
opacity: isNavItemActive(item) ? 1 : 0
|
|
}}
|
|
whileHover={{
|
|
width: "100%",
|
|
opacity: 1
|
|
}}
|
|
transition={{ duration: 0.2 }}
|
|
/>
|
|
{/* Hover background */}
|
|
<motion.div
|
|
className="absolute inset-0 bg-gray-100/50 backdrop-blur-sm rounded-lg -z-10"
|
|
initial={{ scale: 0, opacity: 0 }}
|
|
whileHover={{ scale: 1, opacity: 0.5 }}
|
|
transition={{ duration: 0.2 }}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Right Section */}
|
|
<div className="flex items-center gap-1">
|
|
{/* City Selector - Uses Navbar Handler */}
|
|
<div
|
|
className="flex items-center space-x-1.5 text-gray-700 hover:text-gray-900 text-base font-medium transition-colors duration-200 cursor-pointer rounded-lg hover:bg-gray-50/50 px-2 py-1"
|
|
onClick={handleOpenCityDialogFromNavbar}
|
|
>
|
|
<span>
|
|
{activeCity &&
|
|
activeCity !== 'shared' &&
|
|
activeCity !== 'Landingpage' &&
|
|
activeCity.toLowerCase() !== 'landingpage' ?
|
|
activeCity.charAt(0).toUpperCase() + activeCity.slice(1) :
|
|
'Select City'
|
|
}
|
|
</span>
|
|
<ChevronDown className="w-3.5 h-3.5" />
|
|
</div>
|
|
|
|
{/* Language Dropdown */}
|
|
<Dropdown
|
|
ref={languageRef}
|
|
isOpen={activeLanguageDropdown}
|
|
onToggle={() => setActiveLanguageDropdown(!activeLanguageDropdown)}
|
|
items={languages}
|
|
title="Select Language"
|
|
trigger={
|
|
<div className="flex items-center space-x-1.5 text-gray-700 hover:text-gray-900 text-base font-medium transition-colors duration-200 cursor-pointer rounded-lg hover:bg-gray-50/50 uppercase px-2 py-1">
|
|
<Globe className="w-4 h-4" />
|
|
<span>ENG</span>
|
|
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${activeLanguageDropdown ? 'rotate-180' : ''}`} />
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
{/* Shopping Cart */}
|
|
<Dropdown
|
|
ref={cartRef}
|
|
isOpen={activeCartDropdown}
|
|
onToggle={() => setActiveCartDropdown(prev => !prev)}
|
|
items={cartDropdownItems}
|
|
title="Shopping Cart"
|
|
trigger={
|
|
<div className="relative text-gray-700 hover:text-gray-900 transition-colors duration-200 rounded-lg hover:bg-gray-50/50 cursor-pointer p-2">
|
|
<ShoppingBag className="w-6 h-6" />
|
|
<motion.div
|
|
className="absolute -top-1 -right-1 w-5 h-5 bg-primary rounded-full flex items-center justify-center"
|
|
animate={{ scale: [1, 1.1, 1] }}
|
|
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
|
>
|
|
<span className="text-xs text-primary-foreground font-bold">{cartItems.length}</span>
|
|
</motion.div>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
{/* Enhanced City Card Button with Source Tracking */}
|
|
<div className="flex items-center gap-3 pl-2">
|
|
<div className="relative">
|
|
{isUserSignedIn && user ? (
|
|
<Dropdown
|
|
ref={userRef}
|
|
isOpen={activeUserDropdown}
|
|
onToggle={() => setActiveUserDropdown(prev => !prev)}
|
|
items={[
|
|
{
|
|
id: 'profile',
|
|
label: 'My Profile',
|
|
icon: <User className="w-4 h-4" />,
|
|
action: () => {
|
|
navigate('/profile');
|
|
setActiveUserDropdown(false);
|
|
}
|
|
},
|
|
{
|
|
id: 'settings',
|
|
label: 'Settings',
|
|
icon: <Settings className="w-4 h-4" />,
|
|
action: () => {
|
|
navigate('/comming-soon');
|
|
setActiveUserDropdown(false);
|
|
}
|
|
},
|
|
{
|
|
id: 'logout',
|
|
label: 'Sign Out',
|
|
icon: <LogOut className="w-4 h-4" />,
|
|
action: () => {
|
|
if (onSignOutClick) {
|
|
onSignOutClick();
|
|
}
|
|
setActiveUserDropdown(false);
|
|
}
|
|
}
|
|
]}
|
|
title="Account"
|
|
trigger={
|
|
<div
|
|
className="cursor-pointer"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setActiveUserDropdown(prev => !prev);
|
|
}}
|
|
>
|
|
<CTAButton
|
|
user={user}
|
|
onClick={() => { }}
|
|
className="hover:scale-105 transition-transform duration-200"
|
|
/>
|
|
</div>
|
|
}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="cursor-pointer"
|
|
onClick={handleOpenCityDialogFromCTA}
|
|
>
|
|
<CTAButton
|
|
user={null}
|
|
onClick={() => { }}
|
|
className="hover:scale-105 transition-transform duration-200"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</motion.nav>
|
|
|
|
{/* Mobile Navbar - Enhanced Glassmorphism */}
|
|
<nav className="fixed top-0 w-full z-50 lg:hidden">
|
|
<div className={`transition-all duration-500 ease-out border-b shadow-sm ${isScrolled
|
|
? 'bg-white/85 backdrop-blur-2xl border-white/40 shadow-black/5'
|
|
: 'bg-white/70 backdrop-blur-3xl border-white/50 shadow-black/10'
|
|
}`}>
|
|
<div className="container mx-auto px-4">
|
|
<div className="flex justify-between items-center h-16">
|
|
{/* Mobile Logo */}
|
|
<motion.div
|
|
className="flex items-center cursor-pointer"
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
>
|
|
<Link to="/">
|
|
<ImageWithFallback
|
|
src={logoImage}
|
|
alt="CityCards Logo"
|
|
className="h-10 w-auto"
|
|
/>
|
|
</Link>
|
|
</motion.div>
|
|
|
|
{/* Mobile Actions */}
|
|
<div className="flex items-center space-x-2">
|
|
{/* Mobile City Selector */}
|
|
<Dropdown
|
|
ref={cityRef}
|
|
isOpen={activeCityDropdown}
|
|
onToggle={() => setActiveCityDropdown(!activeCityDropdown)}
|
|
items={cities.map(city => ({
|
|
id: 'city-change',
|
|
label: city.label,
|
|
action: () => handleMobileCityChange(city.id)
|
|
}))}
|
|
title="Select City"
|
|
trigger={
|
|
<div className="flex items-center space-x-1 text-gray-700 hover:text-gray-900 text-sm font-medium transition-colors duration-200 cursor-pointer rounded-lg hover:bg-gray-50/50 px-2 py-1">
|
|
<span>
|
|
{activeCity && activeCity !== 'shared' ?
|
|
activeCity.charAt(0).toUpperCase() + activeCity.slice(1) :
|
|
currentSource === 'melbourne' ? 'Melbourne' : 'Select City'
|
|
}
|
|
</span>
|
|
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${activeCityDropdown ? 'rotate-180' : ''}`} />
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
{/* Mobile Cart */}
|
|
<motion.button
|
|
className="relative text-gray-700 hover:text-gray-900 p-2 transition-colors duration-200 rounded-lg hover:bg-gray-50/50"
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={() => navigate('/checkout')}
|
|
>
|
|
<ShoppingBag className="w-6 h-6" />
|
|
<div className="absolute -top-1 -right-1 w-5 h-5 bg-primary rounded-full flex items-center justify-center">
|
|
<span className="text-xs text-primary-foreground font-bold">{cartItems.length}</span>
|
|
</div>
|
|
</motion.button>
|
|
|
|
{/* Mobile menu button */}
|
|
<motion.button
|
|
onClick={() => setIsMobileMenuOpen(true)}
|
|
className="inline-flex items-center justify-center p-2 rounded-lg text-gray-700 hover:text-gray-900 hover:bg-gray-100/50 transition-colors duration-200"
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
>
|
|
<Menu className="h-6 w-6" />
|
|
</motion.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Enhanced Mobile Menu Overlay */}
|
|
<AnimatePresence>
|
|
{isMobileMenuOpen && (
|
|
<>
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.3 }}
|
|
className="fixed inset-0 bg-black/70 backdrop-blur-lg z-50 lg:hidden"
|
|
onClick={closeMobileMenu}
|
|
/>
|
|
<motion.div
|
|
initial={{ x: '100%' }}
|
|
animate={{ x: 0 }}
|
|
exit={{ x: '100%' }}
|
|
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
|
|
className="fixed top-0 right-0 h-full w-80 bg-white/95 backdrop-blur-2xl shadow-2xl z-50 lg:hidden overflow-y-auto"
|
|
>
|
|
<div className="flex items-center justify-between p-6 border-b border-gray-200/50">
|
|
<h2 className="text-xl font-semibold text-gray-900">Menu</h2>
|
|
<motion.button
|
|
onClick={closeMobileMenu}
|
|
className="p-2 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100/50 transition-colors duration-200"
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
>
|
|
<X className="h-6 w-6" />
|
|
</motion.button>
|
|
</div>
|
|
|
|
<div className="p-6 space-y-6">
|
|
{/* Enhanced Navigation Links with source tracking */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-gray-900 px-4">Navigation</h3>
|
|
{navigationItems.map((item) => {
|
|
const source = getNavigationSource(item);
|
|
|
|
return (
|
|
<motion.button
|
|
key={`${item.path}-${source}-${item.id}`}
|
|
onClick={() => handleNavClick(item.path)}
|
|
className={`w-full flex items-center justify-between py-3 px-4 rounded-lg text-left transition-colors duration-200 ${isNavItemActive(item)
|
|
? 'bg-primary/10 text-primary font-medium'
|
|
: 'text-gray-700 hover:bg-gray-100/70 hover:text-gray-900'
|
|
}`}
|
|
whileHover={{ x: 4 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
>
|
|
<span className="text-base">{item.displayLabel}</span>
|
|
</motion.button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* City Selection in Mobile Menu */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-gray-900 px-4">Select City</h3>
|
|
{cities.map((city) => (
|
|
<motion.button
|
|
key={city.id}
|
|
onClick={() => handleMobileCityChange(city.id)}
|
|
className={`w-full flex items-center justify-between py-3 px-4 rounded-lg text-left transition-colors duration-200 ${activeCity === city.id
|
|
? 'bg-primary/10 text-primary font-medium'
|
|
: 'text-gray-700 hover:bg-gray-100/70 hover:text-gray-900'
|
|
}`}
|
|
whileHover={{ x: 4 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
>
|
|
<span className="text-base">{city.label}</span>
|
|
</motion.button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Mobile CTA Button */}
|
|
<Button
|
|
onClick={handleOpenCityDialogFromCTA}
|
|
withShine={true}
|
|
size="lg"
|
|
className="min-w-[180px] font-poppins font-semibold rounded-full"
|
|
>
|
|
GET A CITY CARD
|
|
</Button>
|
|
</div>
|
|
</motion.div>
|
|
</>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Enhanced City Selection Dialog */}
|
|
<CitySelectionDialog
|
|
isOpen={isCityDialogOpen}
|
|
onClose={handleCloseCityDialog}
|
|
onCitySelect={handleCitySelect}
|
|
/>
|
|
</>
|
|
);
|
|
} |