get a city card flow fix
This commit is contained in:
16
src/App.tsx
16
src/App.tsx
@@ -24,22 +24,24 @@ function App() {
|
||||
const [offersSource, setOffersSource] = useState<'products' | 'passes'>('products');
|
||||
const [stickyCardType, setStickyCardType] = useState<'unlimited' | 'selective'>('unlimited');
|
||||
|
||||
// Login state management
|
||||
// ✅ Authentication state management
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Login handlers
|
||||
// ✅ Login handlers
|
||||
const handleLoginSuccess = (userData: User) => {
|
||||
setUser(userData);
|
||||
setShowLoginModal(false);
|
||||
console.log('User logged in successfully:', userData);
|
||||
};
|
||||
|
||||
const handleSignOut = () => {
|
||||
setUser(null);
|
||||
navigate('/');
|
||||
console.log('User signed out');
|
||||
};
|
||||
|
||||
const handleCloseLoginModal = () => {
|
||||
@@ -50,6 +52,13 @@ function App() {
|
||||
setShowLoginModal(true);
|
||||
};
|
||||
|
||||
// ✅ Handle checkout (you can expand this later)
|
||||
const handleCheckoutClick = () => {
|
||||
console.log('Proceeding to checkout for user:', user);
|
||||
// Add your checkout logic here
|
||||
navigate('/checkout');
|
||||
};
|
||||
|
||||
// Detect mobile for optimized animations
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
@@ -108,13 +117,14 @@ function App() {
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3, ease: easeOutCubic }}
|
||||
>
|
||||
<AppRouter
|
||||
<AppRouter
|
||||
user={user}
|
||||
showLoginModal={showLoginModal}
|
||||
onSignInClick={handleSignInClick}
|
||||
onSignOutClick={handleSignOut}
|
||||
onLoginSuccess={handleLoginSuccess}
|
||||
onCloseLoginModal={handleCloseLoginModal}
|
||||
onCheckoutClick={handleCheckoutClick} // ✅ Pass checkout handler
|
||||
offersSource={offersSource}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
@@ -45,6 +45,7 @@ interface AppRouterProps {
|
||||
onSignOutClick: () => void;
|
||||
onLoginSuccess: (userData: User) => void;
|
||||
onCloseLoginModal: () => void;
|
||||
onCheckoutClick?: () => void;
|
||||
offersSource: 'products' | 'passes';
|
||||
}
|
||||
|
||||
@@ -55,6 +56,7 @@ export function AppRouter({
|
||||
onSignOutClick,
|
||||
onLoginSuccess,
|
||||
onCloseLoginModal,
|
||||
onCheckoutClick,
|
||||
offersSource
|
||||
}: AppRouterProps) {
|
||||
const location = useLocation();
|
||||
@@ -88,9 +90,13 @@ export function AppRouter({
|
||||
} />
|
||||
|
||||
{/* Passes Route */}
|
||||
<Route path="/passes" element={
|
||||
<Route path="/passes" element={
|
||||
<motion.div key="passes" {...pageTransition}>
|
||||
<PassesPage {...commonNavHandlers} />
|
||||
<PassesPage
|
||||
{...commonNavHandlers}
|
||||
onCheckoutClick={onCheckoutClick} // ✅ Explicitly pass checkout handler
|
||||
onLoginSuccess={onLoginSuccess} // ✅ Pass login success handler
|
||||
/>
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
|
||||
@@ -18,32 +18,33 @@ import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface CheckoutPageProps {
|
||||
onBackClick: () => void;
|
||||
onHomeClick: () => void;
|
||||
onMelbourneClick: () => void;
|
||||
onPassesClick: () => void;
|
||||
onCheckoutClick: () => void;
|
||||
onSignInClick: () => void;
|
||||
onBackClick?: () => void;
|
||||
onHomeClick?: () => void;
|
||||
onMelbourneClick?: () => void;
|
||||
onPassesClick?: () => void;
|
||||
onCheckoutClick?: () => void;
|
||||
onSignInClick?: () => void;
|
||||
onSignOutClick?: () => void;
|
||||
onAttractionsClick: () => void;
|
||||
onBlogsClick: () => void;
|
||||
onHowItWorksClick: () => void;
|
||||
onFAQClick: () => void;
|
||||
onPrivacyPolicyClick: () => void;
|
||||
onAboutUsClick: () => void;
|
||||
onProfileClick: () => void;
|
||||
onCityCardsClick: () => void;
|
||||
onMagicItineraryClick: () => void;
|
||||
onPostCardsClick: () => void;
|
||||
onOffersClick: () => void;
|
||||
onAttractionsClick?: () => void;
|
||||
onBlogsClick?: () => void;
|
||||
onHowItWorksClick?: () => void;
|
||||
onFAQClick?: () => void;
|
||||
onPrivacyPolicyClick?: () => void;
|
||||
onAboutUsClick?: () => void;
|
||||
onProfileClick?: () => void;
|
||||
onCityCardsClick?: () => void;
|
||||
onMagicItineraryClick?: () => void;
|
||||
onPostCardsClick?: () => void;
|
||||
onOffersClick?: () => void;
|
||||
onSecureCheckoutClick?: () => void;
|
||||
onContactUsClick?: () => void;
|
||||
onEsimsClick?: () => void;
|
||||
onHotelDiscountsClick?: () => void;
|
||||
currentPage: string;
|
||||
currentPage?: string;
|
||||
user?: { email: string; name: string } | null;
|
||||
}
|
||||
|
||||
|
||||
// Mock cart data
|
||||
const mockCartItems = [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// CitySelectionDialog.tsx
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom'; // ✅ import navigation hook
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Dialog, DialogContent, DialogTitle, DialogDescription } from './ui/dialog';
|
||||
import { ArrowLeft, Search } from 'lucide-react';
|
||||
import { Input } from './ui/input';
|
||||
@@ -15,6 +16,7 @@ interface City {
|
||||
interface CitySelectionDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCitySelect?: (city: string) => void; // ✅ New prop for city selection callback
|
||||
}
|
||||
|
||||
const cities: City[] = [
|
||||
@@ -28,9 +30,13 @@ const cities: City[] = [
|
||||
{ id: 'louisiana', name: 'Louisiana', imageUrl: 'https://images.unsplash.com/photo-1646508262200-455d62c22182?...' },
|
||||
];
|
||||
|
||||
export function CitySelectionDialog({ isOpen, onClose }: CitySelectionDialogProps) {
|
||||
export function CitySelectionDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onCitySelect
|
||||
}: CitySelectionDialogProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const navigate = useNavigate(); // ✅ navigation hook
|
||||
const navigate = useNavigate();
|
||||
|
||||
const filteredCities = cities.filter(city =>
|
||||
city.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
@@ -38,16 +44,22 @@ export function CitySelectionDialog({ isOpen, onClose }: CitySelectionDialogProp
|
||||
|
||||
const handleCityClick = (city: City) => {
|
||||
console.log('Selected city:', city.name);
|
||||
|
||||
// ✅ Call the onCitySelect callback if provided
|
||||
if (onCitySelect) {
|
||||
onCitySelect(city.id);
|
||||
} else {
|
||||
// ✅ Default behavior: navigate to passes page
|
||||
navigate(`/passes?city=${encodeURIComponent(city.name)}`);
|
||||
}
|
||||
|
||||
onClose();
|
||||
|
||||
// ✅ navigate to /passes with selected city info (optional query param)
|
||||
navigate(`/passes?city=${encodeURIComponent(city.name)}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md w-full p-0 gap-0 font-poppins">
|
||||
{/* Accessible Title */}
|
||||
{/* ... rest of the component remains the same ... */}
|
||||
<DialogTitle className="sr-only">Select a City</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Choose from our available cities to explore attractions and experiences
|
||||
@@ -86,7 +98,7 @@ export function CitySelectionDialog({ isOpen, onClose }: CitySelectionDialogProp
|
||||
{filteredCities.map((city, index) => (
|
||||
<motion.button
|
||||
key={city.id}
|
||||
onClick={() => handleCityClick(city)} // ✅ navigate on click
|
||||
onClick={() => handleCityClick(city)}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
@@ -111,7 +123,6 @@ export function CitySelectionDialog({ isOpen, onClose }: CitySelectionDialogProp
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* No Results */}
|
||||
{filteredCities.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500 font-poppins">
|
||||
@@ -123,4 +134,4 @@ export function CitySelectionDialog({ isOpen, onClose }: CitySelectionDialogProp
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -242,18 +242,18 @@ function HeroBannerCarousel({ onCheckoutClick, onPassesClick, onEsimsClick, onHo
|
||||
}
|
||||
|
||||
interface MelbournePageProps {
|
||||
onBackClick: () => void;
|
||||
onHomeClick: () => void;
|
||||
onAttractionsClick: () => void;
|
||||
onPassesClick: () => void;
|
||||
onBackClick?: () => void;
|
||||
onHomeClick?: () => void;
|
||||
onAttractionsClick?: () => void;
|
||||
onPassesClick?: () => void;
|
||||
onCheckoutClick?: () => void;
|
||||
onSignInClick: () => void;
|
||||
onSignInClick?: () => void;
|
||||
onSignOutClick?: () => void;
|
||||
onBlogsClick: () => void;
|
||||
onHowItWorksClick: () => void;
|
||||
onFAQClick: () => void;
|
||||
onPrivacyPolicyClick: () => void;
|
||||
onAboutUsClick: () => void;
|
||||
onBlogsClick?: () => void;
|
||||
onHowItWorksClick?: () => void;
|
||||
onFAQClick?: () => void;
|
||||
onPrivacyPolicyClick?: () => void;
|
||||
onAboutUsClick?: () => void;
|
||||
onProfileClick?: () => void;
|
||||
onCityCardsClick?: () => void;
|
||||
onMagicItineraryClick?: () => void;
|
||||
@@ -266,6 +266,7 @@ interface MelbournePageProps {
|
||||
user?: User | null;
|
||||
}
|
||||
|
||||
|
||||
export function MelbournePage({
|
||||
onBackClick,
|
||||
onHomeClick,
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function Navbar({
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOpenCityDialog = () => {
|
||||
const handleOpenCityDialog = () => {
|
||||
setIsCityDialogOpen(true);
|
||||
};
|
||||
|
||||
@@ -77,6 +77,13 @@ export default function Navbar({
|
||||
setIsCityDialogOpen(false);
|
||||
};
|
||||
|
||||
// ✅ Handle city selection from dialog
|
||||
const handleCitySelect = (cityId: string) => {
|
||||
console.log('City selected in navbar:', cityId);
|
||||
// Navigate to passes page when city is selected
|
||||
navigate('/passes');
|
||||
};
|
||||
|
||||
// Available cities
|
||||
const cities = [
|
||||
{ id: 'melbourne', label: 'Melbourne' },
|
||||
@@ -85,6 +92,7 @@ export default function Navbar({
|
||||
{ id: 'perth', label: 'Perth' }
|
||||
];
|
||||
|
||||
|
||||
// Check if we're on landing page
|
||||
const isLandingPage = location.pathname === '/';
|
||||
|
||||
@@ -341,7 +349,6 @@ export default function Navbar({
|
||||
}
|
||||
className="h-10 w-auto"
|
||||
/>
|
||||
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
@@ -359,7 +366,6 @@ export default function Navbar({
|
||||
}`}
|
||||
>
|
||||
{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"
|
||||
@@ -374,7 +380,6 @@ export default function Navbar({
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
|
||||
{/* Hover background */}
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gray-100/50 backdrop-blur-sm rounded-lg -z-10"
|
||||
@@ -384,12 +389,10 @@ export default function Navbar({
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Right Section */}
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
{/* City Dropdown */}
|
||||
<Dropdown
|
||||
ref={cityRef}
|
||||
@@ -421,6 +424,7 @@ export default function Navbar({
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Language Dropdown */}
|
||||
<Dropdown
|
||||
ref={languageRef}
|
||||
@@ -437,12 +441,12 @@ export default function Navbar({
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Shopping Cart - UPDATED: using cartDropdownItems */}
|
||||
{/* Shopping Cart */}
|
||||
<Dropdown
|
||||
ref={cartRef}
|
||||
isOpen={activeCartDropdown}
|
||||
onToggle={() => setActiveCartDropdown(prev => !prev)}
|
||||
items={cartDropdownItems} // Using the updated array with navigation
|
||||
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">
|
||||
@@ -457,64 +461,86 @@ export default function Navbar({
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* City Card Button */}
|
||||
{/* ✅ UPDATED: City Card Button with Proper Authentication Logic */}
|
||||
<div className="flex items-center gap-3 pl-2">
|
||||
<div className="relative">
|
||||
<CTAButton
|
||||
user={user ?? null}
|
||||
// 👇 open the city selection dialog on click
|
||||
onClick={handleOpenCityDialog}
|
||||
className="hover:scale-105 transition-transform duration-200"
|
||||
/>
|
||||
<div className="relative">
|
||||
{isUserSignedIn && user ? (
|
||||
// ✅ When user is logged in - show user dropdown
|
||||
<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={() => { }} // Empty function since we handle click above
|
||||
className="hover:scale-105 transition-transform duration-200"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
// ✅ When user is NOT logged in - show city selection dialog
|
||||
<>
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={handleOpenCityDialog}
|
||||
>
|
||||
<CTAButton
|
||||
user={null}
|
||||
onClick={() => { }} // Empty function since we handle click above
|
||||
className="hover:scale-105 transition-transform duration-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ✅ City Selection Dialog */}
|
||||
<CitySelectionDialog
|
||||
isOpen={isCityDialogOpen}
|
||||
onClose={handleCloseCityDialog}
|
||||
/>
|
||||
|
||||
{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('/settings');
|
||||
setActiveUserDropdown(false);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'logout',
|
||||
label: 'Sign Out',
|
||||
icon: <LogOut className="w-4 h-4" />,
|
||||
action: () => {
|
||||
if (onSignOutClick) {
|
||||
onSignOutClick();
|
||||
}
|
||||
setActiveUserDropdown(false);
|
||||
},
|
||||
},
|
||||
]}
|
||||
title="Account"
|
||||
trigger={null}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* ✅ City Selection Dialog with navigation to passes */}
|
||||
<CitySelectionDialog
|
||||
isOpen={isCityDialogOpen}
|
||||
onClose={handleCloseCityDialog}
|
||||
onCitySelect={handleCitySelect}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -522,6 +548,7 @@ export default function Navbar({
|
||||
</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
|
||||
|
||||
@@ -5,17 +5,18 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/
|
||||
import { RadioGroup, RadioGroupItem } from './ui/radio-group';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { CitySubmenu } from './CitySubmenu';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { Footer } from './Footer';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
import { Layout } from '../Layout';
|
||||
import { LoginModal } from './LoginModal';
|
||||
|
||||
interface PassesPageProps {
|
||||
onCheckoutClick?: () => void;
|
||||
onSignInClick: () => void;
|
||||
onSignOutClick?: () => void;
|
||||
user?: { email: string; name: string; } | null;
|
||||
onLoginSuccess?: (userData: { email: string; name: string }) => void;
|
||||
}
|
||||
|
||||
interface PassType {
|
||||
@@ -45,7 +46,7 @@ const passTypes: PassType[] = [
|
||||
{
|
||||
id: 'selective',
|
||||
name: 'Selective Pass',
|
||||
title: 'SELECTIVE PASS',
|
||||
title: 'Flexi Card',
|
||||
description: 'Perfect for travelers who want to explore selected attractions at their own pace with essential features.',
|
||||
price: '$59.99',
|
||||
originalPrice: '$89.99',
|
||||
@@ -144,12 +145,38 @@ const trustFeatures = [
|
||||
];
|
||||
|
||||
export function PassesPage({
|
||||
onCheckoutClick,
|
||||
onSignInClick,
|
||||
onCheckoutClick,
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
user
|
||||
user,
|
||||
onLoginSuccess
|
||||
}: PassesPageProps) {
|
||||
const [selectedPass, setSelectedPass] = useState<string>('unlimited');
|
||||
const [isLoginOpen, setIsLoginOpen] = useState(false);
|
||||
const [userData, setUserData] = useState<{ email: string; name: string } | null>(user || null);
|
||||
|
||||
// ✅ Handle purchase button click
|
||||
const handlePurchaseClick = () => {
|
||||
if (!userData) {
|
||||
// User not logged in - show login modal
|
||||
setIsLoginOpen(true);
|
||||
} else {
|
||||
// User is logged in - proceed to checkout
|
||||
onCheckoutClick?.();
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ Handle successful login
|
||||
const handleLoginSuccess = (data: { email: string; name: string }) => {
|
||||
setUserData(data);
|
||||
setIsLoginOpen(false);
|
||||
console.log('Logged in user:', data);
|
||||
|
||||
// Call parent's onLoginSuccess if provided
|
||||
if (onLoginSuccess) {
|
||||
onLoginSuccess(data);
|
||||
}
|
||||
};
|
||||
|
||||
const renderFeatureValue = (value: boolean | string) => {
|
||||
if (typeof value === 'boolean') {
|
||||
@@ -164,18 +191,15 @@ export function PassesPage({
|
||||
|
||||
return (
|
||||
<Layout
|
||||
activeCity="Melbourne"
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
showCitySubmenu={true}
|
||||
>
|
||||
|
||||
activeCity="Melbourne"
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={userData} // ✅ Pass the updated user data
|
||||
>
|
||||
<div className="container mx-auto px-4 pt-52 pb-12 relative z-10">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-16">
|
||||
<div className="mb-6">
|
||||
|
||||
<h1 className="font-merchant font-light text-4xl md:text-5xl lg:text-6xl mb-4">
|
||||
Buy <span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">Passes</span>
|
||||
</h1>
|
||||
@@ -187,21 +211,20 @@ export function PassesPage({
|
||||
|
||||
{/* Pass Comparison Section */}
|
||||
<div className="mb-20">
|
||||
<RadioGroup
|
||||
value={selectedPass}
|
||||
<RadioGroup
|
||||
value={selectedPass}
|
||||
onValueChange={setSelectedPass}
|
||||
className="grid md:grid-cols-2 gap-8 max-w-6xl mx-auto"
|
||||
>
|
||||
{passTypes.map((pass) => (
|
||||
<div key={pass.id} className="relative h-full">
|
||||
<Card className={`relative h-full flex flex-col transition-all duration-300 cursor-pointer ${
|
||||
pass.popular
|
||||
? 'ring-2 ring-primary shadow-xl'
|
||||
: selectedPass === pass.id
|
||||
? 'ring-2 ring-primary/50 shadow-lg'
|
||||
: 'border-gray-200 shadow-md hover:shadow-lg hover:border-primary/30'
|
||||
}`}>
|
||||
|
||||
<Card className={`relative h-full flex flex-col transition-all duration-300 cursor-pointer ${pass.popular
|
||||
? 'ring-2 ring-primary shadow-xl'
|
||||
: selectedPass === pass.id
|
||||
? 'ring-2 ring-primary/50 shadow-lg'
|
||||
: 'border-gray-200 shadow-md hover:shadow-lg hover:border-primary/30'
|
||||
}`}>
|
||||
|
||||
{/* Popular Badge */}
|
||||
{pass.popular && (
|
||||
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2 z-10">
|
||||
@@ -225,7 +248,7 @@ export function PassesPage({
|
||||
{pass.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
|
||||
{/* Pricing Section - Fixed Height */}
|
||||
<div className="px-6 pb-6 flex-shrink-0">
|
||||
<div className="flex items-baseline justify-center gap-2 mb-2">
|
||||
@@ -241,7 +264,7 @@ export function PassesPage({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Content - Flexible Height with Fixed Features Area */}
|
||||
<CardContent className="pt-0 pb-6 px-6 flex-grow flex flex-col">
|
||||
{/* Features List - Fixed height */}
|
||||
@@ -256,19 +279,18 @@ export function PassesPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Button - Pushed to bottom */}
|
||||
{/* ✅ UPDATED: CTA Button with login logic */}
|
||||
<div className="flex-shrink-0 space-y-3">
|
||||
<Button
|
||||
className={`w-full h-12 rounded-lg font-semibold transition-all duration-300 font-poppins ${
|
||||
pass.popular
|
||||
? 'bg-primary hover:bg-primary/90 text-white shadow-md hover:shadow-lg'
|
||||
: 'bg-gray-900 hover:bg-gray-800 text-white hover:shadow-md'
|
||||
}`}
|
||||
onClick={onCheckoutClick}
|
||||
<Button
|
||||
className={`w-full h-12 rounded-lg font-semibold transition-all duration-300 font-poppins ${pass.popular
|
||||
? 'bg-primary hover:bg-primary/90 text-white shadow-md hover:shadow-lg'
|
||||
: 'bg-gray-900 hover:bg-gray-800 text-white hover:shadow-md'
|
||||
}`}
|
||||
onClick={handlePurchaseClick} // ✅ Use the new handler
|
||||
>
|
||||
PURCHASE NOW
|
||||
</Button>
|
||||
|
||||
|
||||
<p className="text-xs text-gray-500 text-center font-poppins font-normal leading-tight">
|
||||
✓ Free cancellation up to 24 hours • Instant delivery
|
||||
</p>
|
||||
@@ -290,7 +312,7 @@ export function PassesPage({
|
||||
See exactly what's included with each pass type
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
@@ -301,7 +323,7 @@ export function PassesPage({
|
||||
<th className="text-center p-6 font-semibold text-gray-900 min-w-[200px] bg-primary/5">Premium Unlimited</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
||||
<tbody>
|
||||
{featureComparison.map((feature, index) => (
|
||||
<tr key={feature.key} className={`border-b border-gray-100 ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'}`}>
|
||||
@@ -330,17 +352,17 @@ export function PassesPage({
|
||||
<Card className="bg-gray-50 overflow-hidden shadow-lg">
|
||||
<CardContent className="p-0">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
|
||||
|
||||
{/* Smartphone Mockup */}
|
||||
<div className="relative flex justify-center p-8 lg:p-16">
|
||||
<div className="relative">
|
||||
{/* Phone Frame */}
|
||||
<div className="w-64 h-[520px] bg-black rounded-[2.5rem] p-2 shadow-2xl">
|
||||
<div className="w-full h-full bg-white rounded-[2rem] relative overflow-hidden">
|
||||
|
||||
|
||||
{/* Screen Content */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-primary/90 to-secondary/90">
|
||||
|
||||
|
||||
{/* Status Bar */}
|
||||
<div className="flex justify-between items-center px-6 pt-4 pb-2 text-white text-sm">
|
||||
<span>9:41</span>
|
||||
@@ -350,7 +372,7 @@ export function PassesPage({
|
||||
<div className="w-4 h-2 bg-white rounded-sm opacity-40"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* App Header */}
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
@@ -363,7 +385,7 @@ export function PassesPage({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Active Pass Card */}
|
||||
<div className="mx-6 mb-6">
|
||||
<div className="bg-white/15 backdrop-blur-md rounded-2xl p-4 border border-white/20">
|
||||
@@ -377,7 +399,7 @@ export function PassesPage({
|
||||
<p className="text-white/80 text-xs">18 of 24 attractions visited</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="px-6 mb-6">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
@@ -393,7 +415,7 @@ export function PassesPage({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Featured Attraction */}
|
||||
<div className="mx-6">
|
||||
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/20">
|
||||
@@ -407,7 +429,7 @@ export function PassesPage({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Bottom Tab Bar */}
|
||||
<div className="absolute bottom-6 left-4 right-4">
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-2 border border-white/20">
|
||||
@@ -425,7 +447,7 @@ export function PassesPage({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-8 lg:p-16">
|
||||
<h2 className="heading-dynamic text-4xl text-gray-900 mb-6">
|
||||
@@ -435,10 +457,10 @@ export function PassesPage({
|
||||
<span className="font-semibold text-emphasis">phone</span>
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 mb-8 leading-relaxed font-light">
|
||||
Download our mobile app for easy access to your passes, maps, and exclusive offers.
|
||||
Download our mobile app for easy access to your passes, maps, and exclusive offers.
|
||||
Never worry about losing your tickets again.
|
||||
</p>
|
||||
|
||||
|
||||
{/* App Features */}
|
||||
<div className="space-y-4 mb-8">
|
||||
{[
|
||||
@@ -453,7 +475,7 @@ export function PassesPage({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Download Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-8">
|
||||
<Button className="bg-black text-white hover:bg-gray-800 flex items-center justify-center gap-3 px-6 py-4 rounded-xl font-semibold">
|
||||
@@ -465,7 +487,7 @@ export function PassesPage({
|
||||
Get it on Google Play
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* QR Code */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 bg-black rounded-xl p-2">
|
||||
@@ -495,7 +517,7 @@ export function PassesPage({
|
||||
We're committed to providing the best city exploration experience with unmatched value and service
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{trustFeatures.map((feature, index) => {
|
||||
const IconComponent = feature.icon;
|
||||
@@ -527,7 +549,7 @@ export function PassesPage({
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent"></div>
|
||||
<div className="absolute top-10 right-10 w-32 h-32 bg-white/5 rounded-full"></div>
|
||||
<div className="absolute bottom-10 left-10 w-24 h-24 bg-white/5 rounded-full"></div>
|
||||
|
||||
|
||||
<div className="relative z-10">
|
||||
<h3 className="heading-dynamic text-4xl mb-4">
|
||||
<span className="font-light">Ready to</span>{' '}
|
||||
@@ -535,13 +557,13 @@ export function PassesPage({
|
||||
<span className="font-semibold">Melbourne?</span>
|
||||
</h3>
|
||||
<p className="text-xl mb-8 max-w-2xl mx-auto opacity-90 font-light">
|
||||
Choose your pass and start discovering amazing attractions with skip-the-line access.
|
||||
Choose your pass and start discovering amazing attractions with skip-the-line access.
|
||||
Money-back guarantee included.
|
||||
</p>
|
||||
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-8">
|
||||
<Button
|
||||
size="lg"
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-white text-primary hover:bg-gray-100 py-4 px-8 rounded-2xl font-semibold text-lg shadow-lg hover:shadow-xl transition-all duration-200"
|
||||
>
|
||||
Choose Your Pass
|
||||
@@ -551,7 +573,7 @@ export function PassesPage({
|
||||
<span className="font-light">No hidden fees • Instant confirmation</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex justify-center items-center gap-8 text-sm text-white/70">
|
||||
<span className="flex items-center gap-2 font-light">
|
||||
<Shield className="w-4 h-4" />
|
||||
@@ -570,6 +592,12 @@ export function PassesPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoginModal
|
||||
isOpen={isLoginOpen}
|
||||
onClose={() => setIsLoginOpen(false)}
|
||||
onLoginSuccess={handleLoginSuccess}
|
||||
/>
|
||||
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog@1.1.6";
|
||||
import { XIcon } from "lucide-react@0.487.0";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user