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

713 lines
25 KiB
TypeScript

import { useState, useEffect, useRef, forwardRef } from "react";
import {
Star,
Menu,
X,
ShoppingBag,
ChevronDown,
Globe,
} from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { ImageWithFallback } from "./figma/ImageWithFallback";
import { Button } from "./ui/button";
import { CitySubmenu } from "./CitySubmenu";
import imgRectangle4429 from "figma:asset/43f3bc1f9c8cc5b8f60f3f6be0bc1ad29eded0d7.png";
import cityCardsLogo from "figma:asset/e961451f70697dd054c4240fc1dcad81e08ce31e.png";
// City list data for right sidebar
const cityList = [
{
id: 1,
name: "Sydney",
attractions: 65,
image:
"https://images.unsplash.com/photo-1695018228065-2e0026c654af?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzeWRuZXklMjBoYXJib3IlMjBicmlkZ2UlMjBvcGVyYSUyMGhvdXNlfGVufDF8fHx8MTc1NjEyNDA2NXww&ixlib=rb-4.1.0&q=80&w=1080",
},
{
id: 2,
name: "Melbourne",
attractions: 48,
image:
"https://images.unsplash.com/photo-1679731980101-503d93bbec27?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjb2ZmZWUlMjBsYW5ld2F5c3xlbnwxfHx8fDE3NTYxMjQwNzN8MA&ixlib=rb-4.1.0&q=80&w=1080",
},
{
id: 3,
name: "Brisbane",
attractions: 32,
image:
"https://images.unsplash.com/photo-1548661625-a30d197ce439?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxicmlzYmFuZSUyMHJpdmVyJTIwY2l0eSUyMHNreWxpbmV8ZW58MXx8fHwxNzU2MTI0MDc3fDA&ixlib=rb-4.1.0&q=80&w=1080",
},
];
// Interface for dropdown items
interface DropdownItem {
id: string;
label: string;
badge?: string;
icon?: React.ReactNode;
action?: () => void;
}
// Forward ref for dropdown component
const Dropdown = forwardRef<
HTMLDivElement,
{
isOpen: boolean;
onToggle: () => void;
items: DropdownItem[];
title: string;
trigger: React.ReactNode;
}
>(({ isOpen, onToggle, items, title, trigger }, ref) => {
return (
<div ref={ref} className="relative">
<div onClick={onToggle}>{trigger}</div>
<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.15,
ease: [0.25, 0.1, 0.25, 1],
}}
className="absolute right-0 mt-2 w-64 bg-white/90 backdrop-blur-md rounded-2xl shadow-xl border border-white/20 py-2 z-50"
>
<div className="px-4 py-2 border-b border-gray-200/50">
<h3 className="font-poppins font-medium text-gray-900 text-sm">
{title}
</h3>
</div>
{items.map((item) => (
<div
key={item.id}
onClick={item.action}
className="px-4 py-3 hover:bg-gray-50/50 cursor-pointer transition-colors duration-150 flex items-center justify-between"
>
<div className="flex items-center gap-2">
{item.icon}
<span className="font-poppins text-gray-700 text-sm font-normal">
{item.label}
</span>
</div>
{item.badge && (
<span className="bg-primary/10 text-primary text-xs px-2 py-1 rounded-full font-medium">
{item.badge}
</span>
)}
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
);
});
Dropdown.displayName = "Dropdown";
interface HeroSectionProps {
onSignInClick?: () => void;
onPassesClick?: () => void;
onMelbourneClick?: () => void;
onAttractionsClick?: () => void;
onHomeClick?: () => void;
onBlogsClick?: () => void;
onHowItWorksClick?: () => void;
currentPage?: string;
}
export function HeroSection({
onSignInClick,
onPassesClick,
onMelbourneClick,
onAttractionsClick,
onHomeClick,
onBlogsClick,
onHowItWorksClick,
currentPage,
}: HeroSectionProps) {
const [hoveredCity, setHoveredCity] = useState<number | null>(
null,
);
const [isScrolled, setIsScrolled] = useState(false);
const [activeLanguageDropdown, setActiveLanguageDropdown] =
useState(false);
const [activeCartDropdown, setActiveCartDropdown] =
useState(false);
const [scrollY, setScrollY] = useState(0);
// Refs for dropdowns and parallax
const languageRef = useRef<HTMLDivElement>(null);
const cartRef = useRef<HTMLDivElement>(null);
const heroRef = useRef<HTMLDivElement>(null);
// Sample cart data
const [cartItems] = useState([
{
id: "1",
name: "Melbourne City Card",
price: "$49",
quantity: 1,
},
{
id: "2",
name: "Sydney City Card",
price: "$59",
quantity: 1,
},
]);
const cartTotal = cartItems.reduce(
(sum, item) =>
sum + parseFloat(item.price.replace("$", "")),
0,
);
// Language options
const languages = [
{
id: "en",
label: "English",
action: () => console.log("English selected"),
},
{
id: "es",
label: "Español",
action: () => console.log("Spanish selected"),
},
{
id: "fr",
label: "Français",
action: () => console.log("French selected"),
},
{
id: "de",
label: "Deutsch",
action: () => console.log("German selected"),
},
];
// Navigation handlers
const handleNavClick = (action: string) => {
switch (action) {
case "about":
console.log("Navigate to About Us");
break;
case "products":
console.log("Navigate to Cities");
break;
case "attractions":
if (onAttractionsClick) {
onAttractionsClick();
}
break;
case "card":
if (onPassesClick) onPassesClick();
break;
case "offer":
console.log("Navigate to Deals");
break;
default:
break;
}
};
const isNavItemActive = (action: string) => {
if (action === "card" && currentPage === "passes")
return true;
if (action === "about" && currentPage === "home")
return true;
if (action === "attractions" && currentPage === "attractions")
return true;
return false;
};
// Handle scroll effects with parallax (throttled for performance)
useEffect(() => {
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
const currentScrollY = window.scrollY;
setIsScrolled(currentScrollY > 20);
setScrollY(currentScrollY);
ticking = false;
});
ticking = true;
}
};
window.addEventListener("scroll", handleScroll, {
passive: true,
});
return () =>
window.removeEventListener("scroll", handleScroll);
}, []);
// Calculate parallax values
const scrollProgress = Math.min(
scrollY / window.innerHeight,
1,
);
const backgroundScale = 1.2 - scrollProgress * 0.2; // Scale from 1.2 to 1
const backgroundY = scrollY * 0.5; // Parallax movement
const borderOpacity = 1 - scrollProgress * 0.3; // Border fade effect
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
languageRef.current &&
!languageRef.current.contains(event.target as Node)
) {
setActiveLanguageDropdown(false);
}
if (
cartRef.current &&
!cartRef.current.contains(event.target as Node)
) {
setActiveCartDropdown(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () =>
document.removeEventListener(
"mousedown",
handleClickOutside,
);
}, []);
return (
<div
ref={heroRef}
className="bg-[#ffffff] relative min-h-screen overflow-hidden"
data-name="MacBook Pro 14' - 2"
>
{/* Background Image with Parallax */}
<motion.div
className="absolute bg-[position:50%_50%] bg-size-cover h-full left-0 top-0 w-full origin-center"
style={{
backgroundImage: `url('${imgRectangle4429}')`,
scale: backgroundScale,
y: backgroundY,
willChange: "transform",
}}
/>
{/* Black Gradient Overlay */}
<div className="absolute h-full left-0 top-0 w-full bg-gradient-to-b from-black/0 to-black/55" />
{/* Main Content Frame with Border Animation */}
<motion.div
className="absolute h-[840.118px] rounded-[56px] top-[100.11px] translate-x-[-50%] w-[1449.56px] max-w-[95vw]"
style={{ left: "calc(50% + 7.086px)" }}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 1.2,
ease: [0.25, 0.1, 0.25, 1],
}}
>
<motion.div
aria-hidden="true"
className="absolute border-2 border-[#ffffff] border-solid inset-0 pointer-events-none rounded-[56px]"
style={{
opacity: borderOpacity,
willChange: "opacity",
}}
initial={{
clipPath: "polygon(0 0, 0 0, 0 100%, 0 100%)", // Start from top-left
}}
animate={{
clipPath: "polygon(0 0, 100% 0, 100% 100%, 0 100%)", // Expand to full
}}
transition={{
duration: 1.2,
ease: [0.25, 0.1, 0.25, 1],
delay: 0.3,
}}
/>
</motion.div>
{/* Glassmorphism Navigation Bar with Menu */}
<motion.nav
className="absolute top-[17.79px] left-[38.31px] right-[38.31px] z-40"
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,
}}
>
<motion.div
className="backdrop-blur-[10px] backdrop-filter bg-[rgba(255,255,255,0.2)] h-[66.549px] rounded-[33.275px] w-full px-8 py-4 shadow-lg shadow-black/5 border border-white/20"
animate={{
scale: isScrolled ? 0.98 : 1,
y: isScrolled ? 2 : 0,
boxShadow: isScrolled
? "0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.04)"
: "0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03)",
}}
transition={{
duration: 0.3,
ease: [0.25, 0.1, 0.25, 1],
}}
>
{/* Navigation Content */}
<div className="flex items-center justify-between w-full h-full">
{/* CityCards Logo Section */}
<motion.div
className="flex items-center cursor-pointer flex-shrink-0"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onHomeClick?.()}
>
<img
src={cityCardsLogo}
alt="CityCards"
className="h-10 w-auto"
/>
</motion.div>
{/* Navigation Links */}
<div className="hidden md:flex items-center gap-[30px] lg:gap-[51px]">
{[
{ label: "About Us", action: "about" },
{ label: "Cities", action: "products" },
{ 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={`relative px-0 py-2 text-base font-medium transition-all duration-200 whitespace-nowrap group capitalize ${
isNavItemActive(item.action)
? "text-white"
: "text-white/80 hover:text-white"
}`}
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-white/10 rounded-lg -z-10"
initial={{ scale: 0, opacity: 0 }}
whileHover={{ scale: 1, opacity: 0.5 }}
transition={{ duration: 0.2 }}
/>
</motion.button>
))}
</div>
{/* Right Section */}
<div className="flex items-center gap-5">
{/* Language Dropdown */}
<div className="hidden lg:block">
<Dropdown
ref={languageRef}
isOpen={activeLanguageDropdown}
onToggle={() =>
setActiveLanguageDropdown(
!activeLanguageDropdown,
)
}
items={languages}
title="Select Language"
trigger={
<div className="flex items-center space-x-2 text-white/80 hover:text-white px-0 py-2 text-base font-medium transition-colors duration-200 cursor-pointer rounded-lg hover:bg-white/10 uppercase">
<Globe className="w-5 h-5" />
<span>ENG</span>
<ChevronDown
className={`w-4 h-4 transition-transform duration-200 ${activeLanguageDropdown ? "rotate-180" : ""}`}
/>
</div>
}
/>
</div>
{/* Shopping Cart */}
<div className="hidden lg:block">
<Dropdown
ref={cartRef}
isOpen={activeCartDropdown}
onToggle={() =>
setActiveCartDropdown(!activeCartDropdown)
}
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: () => console.log("Checkout"),
},
]}
title="Shopping Cart"
trigger={
<div className="relative text-white/80 hover:text-white p-0 transition-colors duration-200 rounded-lg hover:bg-white/10 cursor-pointer">
<ShoppingBag className="w-7 h-7" />
<motion.div
className="absolute -top-2 -right-2 w-6 h-6 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>
}
/>
</div>
{/* Mobile Menu Button */}
<div className="md:hidden">
<button
onClick={() => console.log('Mobile menu toggled')}
className="text-white/80 hover:text-white p-2"
>
<Menu className="w-6 h-6" />
</button>
</div>
{/* CTA Button */}
<Button
onClick={onSignInClick}
withShine={true}
className="hidden md:block h-[52px] min-w-[120px] lg:min-w-[180px] px-4 lg:px-6 py-4 rounded-full text-white font-medium bg-gradient-to-r from-primary to-secondary text-sm lg:text-base"
>
GET A CITY CARD
</Button>
</div>
</div>
</motion.div>
</motion.nav>
{/* City Submenu */}
<CitySubmenu
onClose={() => {}} // Empty function since it's always shown on homepage
currentPage={currentPage}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onAttractionsClick={onAttractionsClick}
onPassesClick={onPassesClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
/>
{/* Main Content Container */}
<div className="relative z-10 min-h-screen flex items-center">
<div className="container mx-auto px-4 sm:px-6 lg:px-12">
<div className="flex justify-between items-center w-full">
{/* Left Content */}
<div className="max-w-4xl xl:max-w-5xl 2xl:max-w-6xl flex-1">
{/* Main Headline */}
<motion.div
className="absolute content-stretch flex flex-col gap-7 items-start justify-start leading-[0] left-[91.29px] not-italic top-[582px] w-[849.639px] max-w-[70vw]"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.8,
delay: 0.5,
ease: [0.25, 0.1, 0.25, 1],
}}
style={{
y: scrollY * -0.1, // Subtle counter-parallax for content
willChange: "transform",
}}
>
<div className="font-merchant leading-[64px] relative shrink-0 text-[#ffffff] text-[48px] sm:text-[52px] md:text-[60px] lg:text-[64px] w-[771.315px] max-w-full">
<p className="mb-0">
<span className="font-light">Melbourne</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">City</span>{' '}
<span className="font-normal">Card.</span>
</p>
<p>
<span className="font-light">See</span>{' '}
<span className="font-semibold text-emphasis">More</span>{', '}
<span className="font-light">Spend</span>{' '}
<span className="font-bold">Less</span>{'.'}
</p>
</div>
<div
className="font-poppins font-normal min-w-full relative shrink-0 text-[22px] sm:text-[18px] md:text-[20px] lg:text-[22px] text-[rgba(255,255,255,0.74)]"
style={{ width: "min-content" }}
>
<p className="leading-[40px]">
Instant QR access to 40+ attractions,
exclusive perks, and savings up to 30%
</p>
</div>
</motion.div>
{/* CTA Button */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.8,
delay: 0.7,
ease: [0.25, 0.1, 0.25, 1],
}}
className="absolute left-[91.29px] top-[834.87px]"
style={{
y: scrollY * -0.05, // Minimal counter-parallax for button
willChange: "transform",
}}
>
<Button
withShine={true}
className="font-poppins font-semibold h-[61.805px] min-w-[262.349px] px-8 py-4 rounded-full text-white bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 transition-all duration-200 text-[20px]"
>
Explore Melbourne
</Button>
</motion.div>
</div>
{/* Right Side City List */}
<div className="hidden xl:block absolute right-[100px] top-[640px]">
<motion.div
className="space-y-6"
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.8,
delay: 0.6,
ease: [0.25, 0.1, 0.25, 1],
}}
style={{
y: scrollY * -0.08, // Subtle counter-parallax for city list
willChange: "transform",
}}
>
{cityList.map((city, index) => (
<motion.div
key={city.id}
className="relative"
onMouseEnter={() => setHoveredCity(city.id)}
onMouseLeave={() => setHoveredCity(null)}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.6,
delay: 0.6 + index * 0.1,
}}
>
{/* City Name */}
<div className="flex items-center cursor-pointer group">
<span className="font-poppins text-white/90 group-hover:text-white transition-colors duration-300 text-lg font-semibold">
{city.name}
</span>
</div>
{/* Hover Card */}
<AnimatePresence>
{hoveredCity === city.id && (
<motion.div
className="absolute right-full top-0 mr-6 z-30"
initial={{
opacity: 0,
scale: 0.9,
x: 10,
}}
animate={{
opacity: 1,
scale: 1,
x: 0,
}}
exit={{
opacity: 0,
scale: 0.9,
x: 10,
}}
transition={{
duration: 0.2,
ease: [0.16, 1, 0.3, 1],
}}
>
<div className="bg-white/95 backdrop-blur-md rounded-2xl p-4 shadow-2xl border border-white/20 min-w-64">
{/* Image */}
<div className="relative h-32 mb-3 rounded-xl overflow-hidden">
<ImageWithFallback
src={city.image}
alt={city.name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
</div>
{/* Content */}
<div className="space-y-2">
<h4 className="text-lg font-semibold text-gray-900">
{city.name}
</h4>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Star className="w-4 h-4 text-primary fill-current" />
<span className="font-medium">
{city.attractions} attractions
</span>
</div>
<p className="text-xs text-gray-500">
Discover the best experiences
</p>
</div>
</div>
{/* Arrow Pointer */}
<div className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-2">
<div className="w-0 h-0 border-t-8 border-b-8 border-l-8 border-transparent border-l-white/95" />
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
))}
</motion.div>
</div>
</div>
</div>
</div>
</div>
);
}