Files
CityCards-Website/src/components/Navbar.tsx
priyanshuvish 94c5f5641c nav changes
2025-11-11 12:04:10 +05:30

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}
/>
</>
);
}