Files
CityCards-Website/src/components/Navbar.tsx
priyanshuvish 97969c079b new src added
2025-10-09 19:03:24 +05:30

666 lines
25 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 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';
interface NavbarProps {
activeCity: string;
onCityChange: (city: string) => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onPassesClick: () => void;
onCheckoutClick?: () => void;
onHomeClick?: () => void;
onAttractionsClick?: () => void;
onBlogsClick?: () => void;
onHowItWorksClick?: () => void;
onFAQClick?: () => void;
onPrivacyPolicyClick?: () => void;
onAboutUsClick?: () => void;
onProfileClick?: () => void;
onCityCardsClick?: () => void;
onMagicItineraryClick?: () => void;
onPostCardsClick?: () => void;
onOffersClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage?: 'home' | 'signin' | 'passes' | 'attractions' | 'checkout' | 'blogs' | 'how-it-works' | 'faq' | 'privacy-policy' | 'about-us' | 'melbourne' | 'profile' | 'citycards' | 'magic-itinerary' | 'postcards' | 'offers' | 'esims' | 'hotel-discounts';
isUserSignedIn?: boolean;
user?: { email: string; name: string } | null;
}
interface DropdownItem {
id: string;
label: string;
icon?: React.ReactNode;
action?: () => void;
badge?: string | number;
}
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;
}
export default function Navbar({
activeCity,
onCityChange,
onSignInClick,
onSignOutClick,
onPassesClick,
onCheckoutClick,
onHomeClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage = 'home',
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 [activeProductsDropdown, setActiveProductsDropdown] = useState(false);
const languageRef = useRef<HTMLDivElement>(null);
const cartRef = useRef<HTMLDivElement>(null);
const userRef = useRef<HTMLDivElement>(null);
const productsRef = useRef<HTMLDivElement>(null);
// 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> },
];
// Products dropdown items
const productsItems: DropdownItem[] = [
{
id: 'citycards',
label: 'CityCards',
action: onCityCardsClick
},
{
id: 'magic-itinerary',
label: 'Magic Itinerary',
action: onMagicItineraryClick
},
{
id: 'postcards',
label: 'Post Cards',
action: onPostCardsClick
},
{
id: 'offers',
label: 'Offers',
action: onOffersClick
},
{
id: 'esims',
label: 'eSIMs',
action: onEsimsClick
}
];
// 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 },
];
// Section IDs for navigation
const sectionIds = [
'hero-section',
'why-choose-section',
'variety-adventures-section',
'how-it-works-section',
'magic-itinerary-section',
'book-attraction-section',
'custom-postcards-section',
'upcoming-cities-section',
'trust-section',
'mobile-app-section'
];
const scrollToSection = (index: number) => {
const sectionId = sectionIds[index];
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
setIsMobileMenuOpen(false);
};
const closeMobileMenu = () => {
setIsMobileMenuOpen(false);
};
// Detect scroll for navbar styling
useEffect(() => {
const handleScroll = () => {
const scrolled = window.scrollY > 20;
setIsScrolled(scrolled);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
// Add a small delay to prevent immediate closure when toggle is clicked
setTimeout(() => {
if (languageRef.current && !languageRef.current.contains(event.target as Node)) {
setActiveLanguageDropdown(false);
}
if (cartRef.current && !cartRef.current.contains(event.target as Node)) {
setActiveCartDropdown(false);
}
if (userRef.current && !userRef.current.contains(event.target as Node)) {
setActiveUserDropdown(false);
}
if (productsRef.current && !productsRef.current.contains(event.target as Node)) {
setActiveProductsDropdown(false);
}
}, 10);
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleNavClick = (action: string) => {
switch (action) {
case 'about':
onAboutUsClick?.();
break;
case 'attractions':
onAttractionsClick?.();
break;
case 'card':
onPassesClick?.();
break;
default:
break;
}
closeMobileMenu();
};
const isNavItemActive = (action: string) => {
if (action === 'about') {
return currentPage === 'about-us';
}
return currentPage === action;
};
// Calculate cart total
const cartTotal = cartItems.reduce((total, item) => {
const price = parseFloat(item.price.replace('$', ''));
return total + (price * item.quantity);
}, 0);
// Dropdown component with proper ref forwarding and glassmorphism
const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(({
isOpen,
onToggle,
items,
trigger,
title,
className = ""
}, ref) => (
<div ref={ref} className={`relative ${className}`} style={{ height: 'auto', minHeight: 'auto' }}>
<motion.button
onClick={onToggle}
className="relative"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{trigger}
</motion.button>
<AnimatePresence>
{isOpen && (
<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 right-0 mt-2 bg-white/95 backdrop-blur-xl rounded-2xl shadow-xl border border-white/20 min-w-[280px] max-w-[320px] overflow-hidden z-50"
style={{
position: 'absolute',
top: '100%',
right: '0',
marginTop: '0.5rem'
}}
>
{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-3 px-2">
{items.map((item, index) => (
<motion.button
key={item.id}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
// Handle action first
if (item.action && item.id !== 'total') {
item.action();
}
// Only close dropdown for actionable items (checkout and regular cart items)
if (item.id === 'checkout' || (item.id !== 'total' && !item.action)) {
setTimeout(() => onToggle(), 100);
}
}}
className={`w-full flex items-center justify-between text-left transition-colors duration-200 ${
item.id === 'checkout'
? 'bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-medium rounded-lg px-4 py-2.5 mb-1 mt-2'
: item.id === 'total'
? 'cursor-default font-semibold border-t border-gray-100/50 pt-4 px-3 py-2'
: 'hover:bg-gray-50/80 px-3 py-2.5 rounded-md'
}`}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
disabled={item.id === 'total'}
>
<div className="flex items-center space-x-3">
{item.icon}
<span className={`text-sm font-medium ${
item.id === 'checkout' ? 'text-white' :
item.id === 'total' ? 'text-gray-900' : 'text-gray-700'
}`}>{item.label}</span>
</div>
{item.badge && (
<span className="bg-primary text-primary-foreground text-xs px-2 py-1 rounded-full">
{item.badge}
</span>
)}
</motion.button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
));
// Set display name for debugging
Dropdown.displayName = 'Dropdown';
return (
<>
{/* Desktop Navbar - Enhanced Glassmorphism */}
<motion.nav
className="fixed top-6 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="container mx-auto px-4">
<motion.div
className={`w-full transition-all duration-500 ease-out rounded-full 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 }}
onClick={() => onHomeClick?.()}
>
<ImageWithFallback
src={logoImage}
alt="CityCards Logo"
className="h-10 w-auto"
/>
</motion.div>
{/* Navigation Links - Centered */}
<div className="absolute left-1/2 -translate-x-1/2 flex items-center gap-[51px]">
{[
{ label: 'About Us', action: 'about' },
{ label: 'Your Card', action: 'card' }
].map((item) => (
<motion.button
key={item.action}
onClick={() => handleNavClick(item.action)}
className={`relative px-0 py-2 text-base font-medium transition-all duration-200 whitespace-nowrap group capitalize ${
isNavItemActive(item.action)
? 'text-primary'
: 'text-gray-700 hover:text-gray-900'
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{item.label}
{/* 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.action) ? "100%" : 0,
opacity: isNavItemActive(item.action) ? 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 }}
/>
</motion.button>
))}
{/* Our Products Dropdown */}
<button
onClick={onCityCardsClick}
className="flex items-center 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"
>
<span>Our Products</span>
</button>
</div>
{/* Right Section */}
<div className="flex items-center gap-5">
{/* 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={[
...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: onCheckoutClick
}
]}
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>
}
/>
{/* City Card Button */}
<div className="flex items-center gap-3">
<div className="relative">
<CTAButton
user={user}
onClick={user ? () => setActiveUserDropdown(prev => !prev) : (onSignInClick || (() => {}))}
className="hover:scale-105 transition-transform duration-200"
/>
{/* User Profile Dropdown attached to CTA Button */}
{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: onProfileClick
},
{
id: 'settings',
label: 'Settings',
icon: <Settings className="w-4 h-4" />
},
{
id: 'logout',
label: 'Sign Out',
icon: <LogOut className="w-4 h-4" />,
action: onSignOutClick
}
]}
title="Account"
trigger={null}
/>
)}
</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 }}
onClick={() => onHomeClick?.()}
>
<ImageWithFallback
src={logoImage}
alt="CityCards Logo"
className="h-10 w-auto"
/>
</motion.div>
{/* Mobile Actions */}
<div className="flex items-center space-x-2">
{/* 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={() => onCheckoutClick?.()}
>
<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>
{/* Mobile Menu Overlay - Enhanced Glassmorphism */}
<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">
{/* Mobile Navigation Links */}
<div className="space-y-4">
{[
{ label: 'About Us', action: 'about' },
{ label: 'Cities', action: 'cities' },
{ label: 'Attractions', action: 'attractions' },
{ label: 'Your Card', action: 'card' },
{ label: 'Deals', action: 'offer' }
].map((item) => (
<motion.button
key={item.action}
onClick={() => handleNavClick(item.action)}
className={`w-full flex items-center justify-between py-3 px-4 rounded-lg text-left transition-colors duration-200 ${
isNavItemActive(item.action)
? '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.label}</span>
</motion.button>
))}
</div>
{/* Mobile Our Products Section */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 px-4">Our Products</h3>
{productsItems.map((item) => (
<motion.button
key={item.id}
onClick={() => {
item.action?.();
closeMobileMenu();
}}
className="w-full flex items-center justify-between py-3 px-4 rounded-lg text-left transition-colors duration-200 text-gray-700 hover:bg-gray-100/70 hover:text-gray-900"
whileHover={{ x: 4 }}
whileTap={{ scale: 0.98 }}
>
<span className="text-base">{item.label}</span>
</motion.button>
))}
</div>
{/* Mobile CTA Button */}
<Button
onClick={() => {
onSignInClick();
closeMobileMenu();
}}
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-medium py-3"
>
GET A CITY CARD
</Button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
);
}