yet to fix explore card
This commit is contained in:
54
package-lock.json
generated
54
package-lock.json
generated
@@ -49,6 +49,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.55.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"recharts": "^2.15.2",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "*",
|
||||
@@ -2901,6 +2902,15 @@
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -3837,6 +3847,44 @@
|
||||
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
|
||||
"integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.9.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz",
|
||||
"integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-smooth": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||
@@ -3972,6 +4020,12 @@
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sonner": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.55.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"recharts": "^2.15.2",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "*",
|
||||
|
||||
1011
src/App.tsx
1011
src/App.tsx
File diff suppressed because it is too large
Load Diff
261
src/AppRouter.tsx
Normal file
261
src/AppRouter.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import { Routes, Route, useParams, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
|
||||
// Import all your pages
|
||||
import { LoginModal } from './components/LoginModal';
|
||||
import { MelbournePage } from './components/MelbournePage';
|
||||
import { PassesPage } from './components/PassesPage';
|
||||
import { AttractionsPage } from './components/AttractionsPage';
|
||||
import { AttractionDetailsPage } from './components/AttractionDetailsPage';
|
||||
import { CheckoutPage } from './components/CheckoutPage';
|
||||
import { SecureCheckoutPage } from './components/SecureCheckoutPage';
|
||||
import { BlogsPage } from './components/BlogsPage';
|
||||
import { BlogDetailsPage } from './components/BlogDetailsPage';
|
||||
import { HowItWorksPage } from './components/HowItWorksPage';
|
||||
import { FAQPage } from './components/FAQPage';
|
||||
import { PrivacyPolicyPage } from './components/PrivacyPolicyPage';
|
||||
import { AboutUsPage } from './components/AboutUsPage';
|
||||
import { ProfilePage } from './components/ProfilePage';
|
||||
import { CreateMagicItineraryPage } from './components/CreateMagicItineraryPage';
|
||||
import { ItineraryViewPage } from './components/ItineraryViewPage';
|
||||
import { OffersPage } from './components/OffersPage';
|
||||
import { CityCardsPage } from './components/CityCardsPage';
|
||||
import { MagicItineraryPage } from './components/MagicItineraryPage';
|
||||
import { PostCardsPage } from './components/PostCardsPage';
|
||||
import { DownloadAppPage } from './components/DownloadAppPage';
|
||||
import { EsimsPage } from './components/EsimsPage';
|
||||
import { HotelDiscountsPage } from './components/HotelDiscountsPage';
|
||||
import { ContactUsPage } from './components/ContactUsPage';
|
||||
|
||||
import { pageTransition } from './utils/animations';
|
||||
import { LandingPage } from './pages/landingPage';
|
||||
import ComingSoonPage from './pages/ComingSoonPage';
|
||||
|
||||
// User type definition
|
||||
interface User {
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AppRouterProps {
|
||||
user: User | null;
|
||||
showLoginModal: boolean;
|
||||
onSignInClick: () => void;
|
||||
onSignOutClick: () => void;
|
||||
onLoginSuccess: (userData: User) => void;
|
||||
onCloseLoginModal: () => void;
|
||||
offersSource: 'products' | 'passes';
|
||||
}
|
||||
|
||||
export function AppRouter({
|
||||
user,
|
||||
showLoginModal,
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
onLoginSuccess,
|
||||
onCloseLoginModal,
|
||||
offersSource
|
||||
}: AppRouterProps) {
|
||||
const location = useLocation();
|
||||
const { attractionId, blogId } = useParams();
|
||||
|
||||
// Common navigation handlers for all pages
|
||||
const commonNavHandlers = {
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
user,
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence mode="wait">
|
||||
<Routes location={location} key={location.pathname}>
|
||||
{/* Landing Page Route */}
|
||||
<Route path="/" element={
|
||||
<motion.div key="landing" {...pageTransition}>
|
||||
<LandingPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
{/* Home Route */}
|
||||
<Route path="/melbourne" element={
|
||||
<motion.div key="home" {...pageTransition}>
|
||||
<MelbournePage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
{/* Passes Route */}
|
||||
<Route path="/passes" element={
|
||||
<motion.div key="passes" {...pageTransition}>
|
||||
<PassesPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
{/* Melbourne Route */}
|
||||
<Route path="/melbourne" element={
|
||||
<motion.div key="melbourne" {...pageTransition}>
|
||||
<MelbournePage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
{/* Attractions Routes */}
|
||||
<Route path="/attractions" element={
|
||||
<motion.div key="attractions" {...pageTransition}>
|
||||
<AttractionsPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/attractions/:attractionId" element={
|
||||
<motion.div key="attraction-details" {...pageTransition}>
|
||||
<AttractionDetailsPage
|
||||
attractionId={attractionId || ''}
|
||||
{...commonNavHandlers}
|
||||
onBackClick={() => navigate(-1)}
|
||||
onCheckoutClick={() => navigate('/checkout')}
|
||||
/>
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
{/* Checkout Routes */}
|
||||
<Route path="/checkout" element={
|
||||
<motion.div key="checkout" {...pageTransition}>
|
||||
<CheckoutPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/secure-checkout" element={
|
||||
<motion.div key="secure-checkout" {...pageTransition}>
|
||||
<SecureCheckoutPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
{/* Blog Routes */}
|
||||
<Route path="/blogs" element={
|
||||
<motion.div key="blogs" {...pageTransition}>
|
||||
<BlogsPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/blogs/:blogId" element={
|
||||
<motion.div key="blog-details" {...pageTransition}>
|
||||
<BlogDetailsPage
|
||||
blogId={blogId || ''}
|
||||
{...commonNavHandlers}
|
||||
/>
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
{/* Information Pages */}
|
||||
<Route path="/how-it-works" element={
|
||||
<motion.div key="how-it-works" {...pageTransition}>
|
||||
<HowItWorksPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/faq" element={
|
||||
<motion.div key="faq" {...pageTransition}>
|
||||
<FAQPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/privacy-policy" element={
|
||||
<motion.div key="privacy-policy" {...pageTransition}>
|
||||
<PrivacyPolicyPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/about-us" element={
|
||||
<motion.div key="about-us" {...pageTransition}>
|
||||
<AboutUsPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
{/* User Routes */}
|
||||
<Route path="/profile" element={
|
||||
<motion.div key="profile" {...pageTransition}>
|
||||
<ProfilePage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
{/* Itinerary Routes */}
|
||||
<Route path="/create-itinerary" element={
|
||||
<motion.div key="create-itinerary" {...pageTransition}>
|
||||
<CreateMagicItineraryPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/itinerary-view" element={
|
||||
<motion.div key="itinerary-view" {...pageTransition}>
|
||||
<ItineraryViewPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/magic-itinerary" element={
|
||||
<motion.div key="magic-itinerary" {...pageTransition}>
|
||||
<MagicItineraryPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
{/* Products Routes */}
|
||||
<Route path="/offers" element={
|
||||
<motion.div key="offers" {...pageTransition}>
|
||||
<OffersPage fromSource={offersSource} {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/citycards" element={
|
||||
<motion.div key="citycards" {...pageTransition}>
|
||||
<CityCardsPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/postcards" element={
|
||||
<motion.div key="postcards" {...pageTransition}>
|
||||
<PostCardsPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/esims" element={
|
||||
<motion.div key="esims" {...pageTransition}>
|
||||
<EsimsPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/hotel-discounts" element={
|
||||
<motion.div key="hotel-discounts" {...pageTransition}>
|
||||
<HotelDiscountsPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/download-app" element={
|
||||
<motion.div key="download-app" {...pageTransition}>
|
||||
<DownloadAppPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/contact-us" element={
|
||||
<motion.div key="contact-us" {...pageTransition}>
|
||||
<ContactUsPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/comming-soon" element={
|
||||
<motion.div key="comming-soon" {...pageTransition}>
|
||||
<ComingSoonPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
|
||||
</Routes>
|
||||
</AnimatePresence>
|
||||
|
||||
<LoginModal
|
||||
isOpen={showLoginModal}
|
||||
onClose={onCloseLoginModal}
|
||||
onLoginSuccess={onLoginSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
54
src/Layout.tsx
Normal file
54
src/Layout.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ReactNode } from 'react';
|
||||
import Navbar from './components/Navbar';
|
||||
import { CitySubmenu } from './components/CitySubmenu';
|
||||
import { Footer } from './components/Footer';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
activeCity?: string;
|
||||
showCitySubmenu?: boolean;
|
||||
onSignInClick: () => void;
|
||||
onSignOutClick?: () => void;
|
||||
user?: User | null;
|
||||
}
|
||||
|
||||
export function Layout({
|
||||
children,
|
||||
activeCity = 'Melbourne',
|
||||
showCitySubmenu = false,
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
user
|
||||
}: LayoutProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
{/* Navbar */}
|
||||
<Navbar
|
||||
activeCity={activeCity}
|
||||
onCityChange={(city) => {
|
||||
// Handle city change if needed
|
||||
}}
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
isUserSignedIn={!!user}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
{/* City Submenu - Conditionally rendered */}
|
||||
{showCitySubmenu && <CitySubmenu onClose={() => {}} />}
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
src/assets/landing-hero.png
Normal file
BIN
src/assets/landing-hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 MiB |
BIN
src/assets/marriott-hotel.png
Normal file
BIN
src/assets/marriott-hotel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -3,64 +3,24 @@ import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Clock, Users, Calendar, MapPin, Star, Check, X, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Separator } from './ui/separator';
|
||||
import { Calendar as CalendarComponent } from './ui/calendar';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { Card, } from './ui/card';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface AttractionDetailsPageProps {
|
||||
attractionId: string;
|
||||
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;
|
||||
onContactUsClick?: () => void;
|
||||
onEsimsClick?: () => void;
|
||||
onHotelDiscountsClick?: () => void;
|
||||
currentPage: string;
|
||||
user?: { email: string; name: string } | null;
|
||||
attractionId: string;
|
||||
}
|
||||
|
||||
export function AttractionDetailsPage({
|
||||
attractionId,
|
||||
onBackClick,
|
||||
onHomeClick,
|
||||
onMelbourneClick,
|
||||
onPassesClick,
|
||||
onCheckoutClick,
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
onAttractionsClick,
|
||||
onBlogsClick,
|
||||
onHowItWorksClick,
|
||||
onFAQClick,
|
||||
onPrivacyPolicyClick,
|
||||
onAboutUsClick,
|
||||
onProfileClick,
|
||||
onCityCardsClick,
|
||||
onMagicItineraryClick,
|
||||
onPostCardsClick,
|
||||
onOffersClick,
|
||||
onContactUsClick,
|
||||
onEsimsClick,
|
||||
onHotelDiscountsClick,
|
||||
currentPage,
|
||||
user,
|
||||
}: AttractionDetailsPageProps) {
|
||||
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||
@@ -114,35 +74,13 @@ export function AttractionDetailsPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f8f8f8]">
|
||||
<Navbar
|
||||
activeCity="Melbourne"
|
||||
onCityChange={(city) => {
|
||||
if (city === 'Melbourne') {
|
||||
onMelbourneClick();
|
||||
}
|
||||
}}
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
onPassesClick={onPassesClick}
|
||||
onCheckoutClick={onCheckoutClick}
|
||||
onHomeClick={onHomeClick}
|
||||
onAttractionsClick={onAttractionsClick}
|
||||
onBlogsClick={onBlogsClick}
|
||||
onHowItWorksClick={onHowItWorksClick}
|
||||
onFAQClick={onFAQClick}
|
||||
onPrivacyPolicyClick={onPrivacyPolicyClick}
|
||||
onAboutUsClick={onAboutUsClick}
|
||||
onProfileClick={onProfileClick}
|
||||
onCityCardsClick={onCityCardsClick}
|
||||
onMagicItineraryClick={onMagicItineraryClick}
|
||||
onPostCardsClick={onPostCardsClick}
|
||||
onOffersClick={onOffersClick}
|
||||
currentPage="attractions"
|
||||
isUserSignedIn={!!user}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
<Layout
|
||||
activeCity=""
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
showCitySubmenu={false}
|
||||
>
|
||||
<div className="container mx-auto px-4 pt-40 pb-16 max-w-6xl">
|
||||
{/* Back Button */}
|
||||
<motion.div
|
||||
@@ -460,15 +398,7 @@ export function AttractionDetailsPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer
|
||||
onHomeClick={onHomeClick}
|
||||
onAboutUsClick={onAboutUsClick}
|
||||
onPrivacyPolicyClick={onPrivacyPolicyClick}
|
||||
onHowItWorksClick={onHowItWorksClick}
|
||||
onFAQClick={onFAQClick}
|
||||
onBlogsClick={onBlogsClick}
|
||||
onContactUsClick={onContactUsClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { Search, Star, Clock, ChevronRight } from 'lucide-react';
|
||||
import { Search, Star, Clock } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import Navbar from './Navbar';
|
||||
import { CitySubmenu } from './CitySubmenu';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -217,75 +216,34 @@ const passTypeCategories = [
|
||||
];
|
||||
|
||||
interface AttractionsPageProps {
|
||||
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;
|
||||
onContactUsClick?: () => void;
|
||||
onEsimsClick?: () => void;
|
||||
onHotelDiscountsClick?: () => void;
|
||||
onAttractionClick?: (attractionId: string) => void;
|
||||
currentPage: string;
|
||||
user: User | null;
|
||||
onSignOutClick?: () => void;
|
||||
user?: User | null;
|
||||
}
|
||||
|
||||
export function AttractionsPage({
|
||||
onBackClick,
|
||||
onHomeClick,
|
||||
onMelbourneClick,
|
||||
onPassesClick,
|
||||
onCheckoutClick,
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
onAttractionsClick,
|
||||
onBlogsClick,
|
||||
onHowItWorksClick,
|
||||
onFAQClick,
|
||||
onPrivacyPolicyClick,
|
||||
onAboutUsClick,
|
||||
onProfileClick,
|
||||
onCityCardsClick,
|
||||
onMagicItineraryClick,
|
||||
onPostCardsClick,
|
||||
onOffersClick,
|
||||
onContactUsClick,
|
||||
onEsimsClick,
|
||||
onHotelDiscountsClick,
|
||||
onAttractionClick,
|
||||
currentPage,
|
||||
user
|
||||
}: AttractionsPageProps) {
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [selectedPassTypes, setSelectedPassTypes] = useState<string[]>([]);
|
||||
|
||||
const filteredAttractions = attractions.filter(attraction => {
|
||||
const matchesSearch = attraction.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
attraction.description.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
attraction.description.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
const matchesCategory = selectedCategories.length === 0 ||
|
||||
selectedCategories.some(cat => {
|
||||
if (cat === 'with-reservation') return attraction.hasReservation;
|
||||
if (cat === 'without-reservation') return !attraction.hasReservation;
|
||||
return attraction.category === cat;
|
||||
});
|
||||
selectedCategories.some(cat => {
|
||||
if (cat === 'with-reservation') return attraction.hasReservation;
|
||||
if (cat === 'without-reservation') return !attraction.hasReservation;
|
||||
return attraction.category === cat;
|
||||
});
|
||||
|
||||
const matchesPassType = selectedPassTypes.length === 0 ||
|
||||
selectedPassTypes.includes(attraction.passType);
|
||||
selectedPassTypes.includes(attraction.passType);
|
||||
|
||||
return matchesSearch && matchesCategory && matchesPassType;
|
||||
});
|
||||
@@ -306,56 +264,27 @@ export function AttractionsPage({
|
||||
);
|
||||
};
|
||||
|
||||
const handleAttractionClick = (attractionId: string) => {
|
||||
navigate(`/attractions/${attractionId}`);
|
||||
};
|
||||
|
||||
const handleCheckoutClick = () => {
|
||||
navigate('/checkout');
|
||||
};
|
||||
|
||||
const showingFrom = 1;
|
||||
const showingTo = Math.min(12, filteredAttractions.length);
|
||||
const totalItems = filteredAttractions.length;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Navbar
|
||||
activeCity="Melbourne"
|
||||
onCityChange={(city) => {
|
||||
if (city === 'Melbourne') {
|
||||
onMelbourneClick();
|
||||
}
|
||||
}}
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
onPassesClick={onPassesClick}
|
||||
onCheckoutClick={onCheckoutClick}
|
||||
onHomeClick={onHomeClick}
|
||||
onAttractionsClick={onAttractionsClick}
|
||||
onBlogsClick={onBlogsClick}
|
||||
onHowItWorksClick={onHowItWorksClick}
|
||||
onFAQClick={onFAQClick}
|
||||
onPrivacyPolicyClick={onPrivacyPolicyClick}
|
||||
onAboutUsClick={onAboutUsClick}
|
||||
onProfileClick={onProfileClick}
|
||||
onCityCardsClick={onCityCardsClick}
|
||||
onMagicItineraryClick={onMagicItineraryClick}
|
||||
onPostCardsClick={onPostCardsClick}
|
||||
onOffersClick={onOffersClick}
|
||||
currentPage="attractions"
|
||||
isUserSignedIn={!!user}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
{/* City Submenu */}
|
||||
<CitySubmenu
|
||||
currentPage={currentPage}
|
||||
onClose={() => {}} // Empty function since submenu always shows on this page
|
||||
onHomeClick={onHomeClick}
|
||||
onMelbourneClick={onMelbourneClick}
|
||||
onAttractionsClick={onAttractionsClick}
|
||||
onPassesClick={onPassesClick}
|
||||
onBlogsClick={onBlogsClick}
|
||||
onHowItWorksClick={onHowItWorksClick}
|
||||
/>
|
||||
|
||||
<Layout
|
||||
activeCity="Melbourne"
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
showCitySubmenu={true}
|
||||
>
|
||||
<div className="container mx-auto px-4 pt-56 pb-16">
|
||||
{/* Breadcrumb */}
|
||||
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="font-merchant text-2xl md:text-3xl lg:text-4xl leading-tight text-gray-900 mb-4">
|
||||
@@ -393,7 +322,7 @@ export function AttractionsPage({
|
||||
{/* Call-to-Action Button */}
|
||||
<Button
|
||||
className="bg-white/90 hover:bg-white text-primary border-2 border-primary hover:border-primary/80 px-8 h-[44px] rounded-lg font-semibold transition-all duration-200 hover:scale-105 font-poppins shadow-lg"
|
||||
onClick={onCheckoutClick}
|
||||
onClick={handleCheckoutClick}
|
||||
>
|
||||
Buy Now
|
||||
</Button>
|
||||
@@ -493,7 +422,7 @@ export function AttractionsPage({
|
||||
>
|
||||
<Card
|
||||
className="overflow-hidden hover:shadow-lg transition-all duration-200 cursor-pointer h-full flex flex-col group hover:border-primary/20"
|
||||
onClick={() => onAttractionClick?.(attraction.id)}
|
||||
onClick={() => handleAttractionClick(attraction.id)}
|
||||
>
|
||||
<div className="aspect-[4/3] relative bg-gray-200 overflow-hidden">
|
||||
<ImageWithFallback
|
||||
@@ -530,11 +459,10 @@ export function AttractionsPage({
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-4 h-4 ${
|
||||
i < Math.floor(attraction.rating)
|
||||
className={`w-4 h-4 ${i < Math.floor(attraction.rating)
|
||||
? 'fill-primary text-primary'
|
||||
: 'text-gray-300'
|
||||
}`}
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
<span className="text-sm font-medium ml-1 text-gray-700 font-poppins">
|
||||
@@ -573,7 +501,7 @@ export function AttractionsPage({
|
||||
className="bg-primary hover:bg-primary/90 text-white font-poppins font-semibold text-xs px-4 min-h-[44px] min-w-[44px] h-[44px] whitespace-nowrap"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCheckoutClick();
|
||||
handleCheckoutClick();
|
||||
}}
|
||||
>
|
||||
Get Pass
|
||||
@@ -589,16 +517,6 @@ export function AttractionsPage({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer
|
||||
onHomeClick={onHomeClick}
|
||||
onAboutUsClick={onAboutUsClick}
|
||||
onPrivacyPolicyClick={onPrivacyPolicyClick}
|
||||
onHowItWorksClick={onHowItWorksClick}
|
||||
onFAQClick={onFAQClick}
|
||||
onBlogsClick={onBlogsClick}
|
||||
onContactUsClick={onContactUsClick}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +1,55 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
interface CitySubmenuProps {
|
||||
onClose: () => void;
|
||||
currentPage?: string;
|
||||
onHomeClick?: () => void;
|
||||
onMelbourneClick?: () => void;
|
||||
onAttractionsClick?: () => void;
|
||||
onPassesClick?: () => void;
|
||||
onBlogsClick?: () => void;
|
||||
onHowItWorksClick?: () => void;
|
||||
}
|
||||
|
||||
interface SubmenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
isCity?: boolean;
|
||||
path?: string;
|
||||
action?: () => void;
|
||||
}
|
||||
|
||||
export function CitySubmenu({
|
||||
onClose,
|
||||
currentPage,
|
||||
onHomeClick,
|
||||
onMelbourneClick,
|
||||
onAttractionsClick,
|
||||
onPassesClick,
|
||||
onBlogsClick,
|
||||
onHowItWorksClick
|
||||
}: CitySubmenuProps) {
|
||||
const [selectedItem, setSelectedItem] = useState<string | null>(null);
|
||||
export function CitySubmenu({ onClose }: CitySubmenuProps) {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [activeItem, setActiveItem] = useState<string | null>(null);
|
||||
|
||||
// Handle scroll effects to match main navbar behavior
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Direct submenu items for Melbourne
|
||||
const submenuItems: SubmenuItem[] = [
|
||||
{
|
||||
id: 'melbourne',
|
||||
label: 'Melbourne',
|
||||
path: '/melbourne'
|
||||
},
|
||||
{
|
||||
id: 'attractions',
|
||||
label: 'Attractions',
|
||||
path: '/attractions'
|
||||
},
|
||||
{
|
||||
id: 'buy-now',
|
||||
label: 'Buy Now',
|
||||
path: '/passes'
|
||||
},
|
||||
{
|
||||
id: 'blogs',
|
||||
label: 'Blogs',
|
||||
path: '/blogs'
|
||||
},
|
||||
{
|
||||
id: 'how-it-works',
|
||||
label: 'How It Works',
|
||||
path: '/how-it-works'
|
||||
}
|
||||
];
|
||||
|
||||
// Handle scroll effects
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrolled = window.scrollY > 20;
|
||||
@@ -43,83 +60,57 @@ export function CitySubmenu({
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const submenuItems: SubmenuItem[] = [
|
||||
{
|
||||
id: 'melbourne',
|
||||
label: 'Melbourne',
|
||||
isCity: true,
|
||||
action: () => {
|
||||
onMelbourneClick?.();
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'attractions',
|
||||
label: 'Attractions',
|
||||
action: () => {
|
||||
onAttractionsClick?.();
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: 'buy-now',
|
||||
label: 'Buy Now',
|
||||
action: () => {
|
||||
onPassesClick?.();
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'blogs',
|
||||
label: 'Blogs',
|
||||
action: () => {
|
||||
onBlogsClick?.();
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'how-it-works',
|
||||
label: 'How It Works',
|
||||
action: () => {
|
||||
onHowItWorksClick?.();
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Update selection based on current page
|
||||
// Determine active item based on current route
|
||||
useEffect(() => {
|
||||
if (currentPage === 'melbourne') {
|
||||
setSelectedItem('melbourne');
|
||||
} else if (currentPage === 'attractions') {
|
||||
setSelectedItem('attractions');
|
||||
} else if (currentPage === 'blogs') {
|
||||
setSelectedItem('blogs');
|
||||
} else if (currentPage === 'how-it-works') {
|
||||
setSelectedItem('how-it-works');
|
||||
} else if (currentPage === 'passes') {
|
||||
setSelectedItem('buy-now');
|
||||
} else {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}, [currentPage]);
|
||||
const currentItem = submenuItems.find(item =>
|
||||
item.path && location.pathname === item.path
|
||||
);
|
||||
setActiveItem(currentItem?.id || null);
|
||||
}, [location.pathname]);
|
||||
|
||||
const handleItemClick = (item: SubmenuItem) => {
|
||||
if (item.isCity) {
|
||||
setSelectedItem(item.id);
|
||||
const handleSubmenuItemClick = (item: SubmenuItem) => {
|
||||
if (item.path) {
|
||||
navigate(item.path);
|
||||
}
|
||||
setActiveItem(item.id);
|
||||
item.action?.();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isItemActive = (item: SubmenuItem) => {
|
||||
return (item.isCity && currentPage === 'melbourne' && item.id === 'melbourne') ||
|
||||
(item.id === 'attractions' && currentPage === 'attractions') ||
|
||||
(item.id === 'buy-now' && currentPage === 'passes') ||
|
||||
(item.id === 'blogs' && currentPage === 'blogs') ||
|
||||
(item.id === 'how-it-works' && currentPage === 'how-it-works');
|
||||
const isSubmenuItemActive = (itemId: string) => {
|
||||
return activeItem === itemId;
|
||||
};
|
||||
|
||||
// Render direct submenu items
|
||||
const renderSubmenu = () => (
|
||||
<div className="flex items-center gap-1">
|
||||
{submenuItems.map((item) => (
|
||||
<motion.button
|
||||
key={item.id}
|
||||
onClick={() => handleSubmenuItemClick(item)}
|
||||
className={`font-poppins font-medium text-base relative px-4 py-2.5 transition-all duration-300 whitespace-nowrap rounded-full ${
|
||||
isSubmenuItemActive(item.id)
|
||||
? 'bg-primary text-white shadow-md'
|
||||
: 'text-gray-700 hover:text-white hover:bg-gray-800'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{item.label}
|
||||
|
||||
{!isSubmenuItemActive(item.id) && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gray-800 rounded-full -z-10"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileHover={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
)}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop Submenu */}
|
||||
@@ -145,33 +136,7 @@ export function CitySubmenu({
|
||||
}}
|
||||
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{submenuItems.map((item) => (
|
||||
<motion.button
|
||||
key={item.id}
|
||||
onClick={() => handleItemClick(item)}
|
||||
className={`font-poppins font-medium text-base relative px-4 py-2.5 transition-all duration-300 whitespace-nowrap rounded-full ${
|
||||
isItemActive(item)
|
||||
? 'bg-primary text-white shadow-md'
|
||||
: 'text-gray-700 hover:text-white hover:bg-gray-800'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{item.label}
|
||||
|
||||
{/* Hover effect for non-active items */}
|
||||
{!isItemActive(item) && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gray-800 rounded-full -z-10"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileHover={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
)}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
{renderSubmenu()}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
@@ -202,9 +167,9 @@ export function CitySubmenu({
|
||||
{submenuItems.map((item) => (
|
||||
<motion.button
|
||||
key={item.id}
|
||||
onClick={() => handleItemClick(item)}
|
||||
onClick={() => handleSubmenuItemClick(item)}
|
||||
className={`relative px-3 py-2 text-sm font-medium transition-all duration-300 whitespace-nowrap rounded-xl flex-shrink-0 ${
|
||||
isItemActive(item)
|
||||
isSubmenuItemActive(item.id)
|
||||
? 'bg-primary text-white shadow-md'
|
||||
: 'text-gray-700 hover:text-white hover:bg-gray-800'
|
||||
}`}
|
||||
@@ -213,8 +178,7 @@ export function CitySubmenu({
|
||||
>
|
||||
{item.label}
|
||||
|
||||
{/* Hover effect for non-active items */}
|
||||
{!isItemActive(item) && (
|
||||
{!isSubmenuItemActive(item.id) && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gray-800 rounded-xl -z-10"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
@@ -251,33 +215,7 @@ export function CitySubmenu({
|
||||
}}
|
||||
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{submenuItems.map((item) => (
|
||||
<motion.button
|
||||
key={item.id}
|
||||
onClick={() => handleItemClick(item)}
|
||||
className={`relative px-4 py-2.5 text-sm font-medium transition-all duration-300 whitespace-nowrap rounded-full ${
|
||||
isItemActive(item)
|
||||
? 'bg-primary text-white shadow-md'
|
||||
: 'text-gray-700 hover:text-white hover:bg-gray-800'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{item.label}
|
||||
|
||||
{/* Hover effect for non-active items */}
|
||||
{!isItemActive(item) && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gray-800 rounded-full -z-10"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileHover={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
)}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
{renderSubmenu()}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</>
|
||||
|
||||
82
src/components/CityTabsNavbar.tsx
Normal file
82
src/components/CityTabsNavbar.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
interface CityTabsNavbarProps {
|
||||
onPageSelect: (page: string) => void;
|
||||
}
|
||||
|
||||
const CityTabsNavbar: React.FC<CityTabsNavbarProps> = ({ onPageSelect }) => {
|
||||
const [selectedCity, setSelectedCity] = useState<string | null>(null);
|
||||
|
||||
const cities = ["Melbourne", "Sydney", "Brisbane"];
|
||||
|
||||
// City-specific submenus
|
||||
const cityMenus: Record<string, string[]> = {
|
||||
Melbourne: ["Attractions", "Passes", "Deals", "Events"],
|
||||
Sydney: ["Attractions", "Tours", "Food & Drinks"],
|
||||
Brisbane: ["Passes", "Nature", "Culture"]
|
||||
};
|
||||
|
||||
const handleCityClick = (city: string) => {
|
||||
setSelectedCity(city);
|
||||
};
|
||||
|
||||
const handleBackToCities = () => {
|
||||
setSelectedCity(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full bg-white shadow-sm border-t border-gray-200">
|
||||
<div className="container mx-auto flex justify-center py-3 gap-8 text-base font-medium">
|
||||
<AnimatePresence mode="wait">
|
||||
{!selectedCity ? (
|
||||
<motion.div
|
||||
key="cities"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="flex gap-8"
|
||||
>
|
||||
{cities.map((city) => (
|
||||
<button
|
||||
key={city}
|
||||
onClick={() => handleCityClick(city)}
|
||||
className="text-gray-700 hover:text-primary transition-colors"
|
||||
>
|
||||
{city}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key={selectedCity}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="flex gap-8 items-center"
|
||||
>
|
||||
<button
|
||||
onClick={handleBackToCities}
|
||||
className="text-sm text-gray-500 hover:text-primary mr-4"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
|
||||
{cityMenus[selectedCity].map((menu) => (
|
||||
<button
|
||||
key={menu}
|
||||
onClick={() => onPageSelect(`${selectedCity}-${menu.toLowerCase()}`)}
|
||||
className="text-gray-700 hover:text-primary transition-colors"
|
||||
>
|
||||
{menu}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CityTabsNavbar;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Camera, ArrowRight, Edit3, Upload, Type, Calendar } from 'lucide-react';
|
||||
import { Camera, ArrowRight, Edit3, Upload, Type, Calendar, Palette, Edit, Stamp } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { motion, useMotionValue, useSpring, useTransform, useInView } from 'motion/react';
|
||||
import { HandwrittenText, useHandwrittenText } from './HandwrittenText';
|
||||
import vintageVector from 'figma:asset/e8091276ed2c976b5c975f21687b0d1702bd9c90.png';
|
||||
import postcardImage from '../assets/eaf15191e9a315d2d4b384ffcb22910687c3d328.png';
|
||||
|
||||
interface EditableCardProps {
|
||||
isEditing: boolean;
|
||||
@@ -66,7 +66,7 @@ export function CustomPostcards() {
|
||||
|
||||
const [postcardData, setPostcardData] = useState({
|
||||
photo: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=600&fit=crop&crop=center",
|
||||
message: "Greetings from paradise!\nThe beaches here are absolutely\nbreathtaking. Wish you were\nhere to enjoy this amazing\nsunset with me.",
|
||||
message: "Greetings from paradise!\\nThe beaches here are absolutely\\nbreathtaking. Wish you were\\nhere to enjoy this amazing\\nsunset with me.",
|
||||
date: "July 2024",
|
||||
addressLabel: "POSTCARD"
|
||||
});
|
||||
@@ -108,7 +108,7 @@ export function CustomPostcards() {
|
||||
}}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
animate={isEditing ? {
|
||||
boxShadow: "0 0 0 2px rgba(99, 102, 241, 0.5), 0 8px 16px rgba(99, 102, 241, 0.3)"
|
||||
boxShadow: "0 0 0 2px rgba(249, 95, 98, 0.5), 0 8px 16px rgba(249, 95, 98, 0.3)"
|
||||
} : {}}
|
||||
>
|
||||
{children}
|
||||
@@ -146,7 +146,7 @@ export function CustomPostcards() {
|
||||
<motion.span
|
||||
className="text-xs text-gray-700 font-medium"
|
||||
animate={isEditing ? {
|
||||
color: ["#374151", "#6366f1", "#374151"]
|
||||
color: ["#374151", "#F95F62", "#374151"]
|
||||
} : {}}
|
||||
transition={{ duration: 1, repeat: isEditing ? Infinity : 0 }}
|
||||
>
|
||||
@@ -270,8 +270,8 @@ export function CustomPostcards() {
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={vintageVector}
|
||||
<ImageWithFallback
|
||||
src="https://images.unsplash.com/photo-1702825328124-dab63d85490e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx2aW50YWdlJTIwbG9nbyUyMHZlY3RvciUyMHBvc3RhbCUyMHN0YW1wfGVufDF8fHx8MTc1ODk5MjExN3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
||||
alt="Vintage logo"
|
||||
className="w-full h-full object-contain"
|
||||
style={{
|
||||
@@ -292,52 +292,34 @@ export function CustomPostcards() {
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Editable Photo Card with realistic mounting - Responsive */}
|
||||
<EditableCard
|
||||
isEditing={editingCard === 'photo'}
|
||||
onEdit={() => setEditingCard(editingCard === 'photo' ? null : 'photo')}
|
||||
{/* For Correspondence Text */}
|
||||
<div
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '33.3%',
|
||||
height: '72.3%',
|
||||
left: '4.9%',
|
||||
top: '7.4%',
|
||||
transform: 'rotate(-0.8deg)'
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
paddingTop: '8%'
|
||||
}}
|
||||
editIcon={<Upload className="w-3 h-3 text-gray-600" />}
|
||||
>
|
||||
<div
|
||||
className="w-full h-full rounded-lg overflow-hidden shadow-lg relative"
|
||||
<p
|
||||
className="font-poppins font-light"
|
||||
style={{
|
||||
border: '4px solid #ffffff',
|
||||
boxShadow: `
|
||||
0px 6px 25px rgba(0, 0, 0, 0.25),
|
||||
inset 0px 1px 3px rgba(255, 255, 255, 0.8),
|
||||
inset 0px -1px 2px rgba(160, 130, 100, 0.2)
|
||||
`
|
||||
fontSize: 'clamp(10px, 2.2vw, 16px)',
|
||||
color: 'rgba(101, 84, 63, 0.4)',
|
||||
letterSpacing: '0.5px',
|
||||
lineHeight: '1.4',
|
||||
textShadow: '0px 0.5px 1px rgba(0, 0, 0, 0.05)'
|
||||
}}
|
||||
>
|
||||
<ImageWithFallback
|
||||
src={postcardData.photo}
|
||||
alt="Beautiful tropical beach"
|
||||
className="w-full h-full object-cover"
|
||||
style={{
|
||||
filter: 'sepia(8%) saturate(110%) contrast(102%) brightness(98%)'
|
||||
}}
|
||||
/>
|
||||
{/* Photo aging overlay */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(circle at 20% 20%, rgba(255, 235, 205, 0.15) 0%, transparent 40%),
|
||||
radial-gradient(circle at 80% 80%, rgba(200, 170, 130, 0.1) 0%, transparent 35%)
|
||||
`,
|
||||
mixBlendMode: 'multiply'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</EditableCard>
|
||||
For correspondence
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Realistic vertical divider with ink bleeding - Responsive */}
|
||||
<div
|
||||
@@ -662,179 +644,303 @@ export function CustomPostcards() {
|
||||
{/* Background decorations */}
|
||||
<div className="absolute inset-0 opacity-10 overflow-hidden">
|
||||
{/* Vintage Stamps */}
|
||||
<div className="absolute top-20 left-20 w-16 h-20 bg-gradient-to-br from-primary/30 to-secondary/30 rounded-sm rotate-12 border-2 border-primary/20"></div>
|
||||
<div className="absolute top-40 right-32 w-12 h-16 bg-gradient-to-br from-secondary/30 to-primary/30 rounded-sm -rotate-6 border-2 border-secondary/20"></div>
|
||||
<div className="absolute bottom-32 left-40 w-14 h-18 bg-gradient-to-br from-primary/20 to-secondary/20 rounded-sm rotate-45 border-2 border-primary/15"></div>
|
||||
<div className="absolute top-20 left-20 w-16 h-20 bg-warm-coral/30 rounded-sm rotate-12 border-2 border-warm-coral/20"></div>
|
||||
<div className="absolute top-40 right-32 w-12 h-16 bg-warm-coral/30 rounded-sm -rotate-6 border-2 border-warm-coral/20"></div>
|
||||
<div className="absolute bottom-32 left-40 w-14 h-18 bg-warm-coral/20 rounded-sm rotate-45 border-2 border-warm-coral/15"></div>
|
||||
|
||||
{/* Paper Textures */}
|
||||
<div className="absolute top-1/3 right-1/4 w-32 h-32 bg-gradient-to-tr from-primary/10 to-transparent rounded-full blur-2xl"></div>
|
||||
<div className="absolute bottom-1/3 left-1/4 w-40 h-40 bg-gradient-to-bl from-secondary/10 to-transparent rounded-full blur-2xl"></div>
|
||||
<div className="absolute top-1/3 right-1/4 w-32 h-32 bg-warm-coral/10 rounded-full blur-2xl"></div>
|
||||
<div className="absolute bottom-1/3 left-1/4 w-40 h-40 bg-warm-coral/10 rounded-full blur-2xl"></div>
|
||||
|
||||
{/* Ink Splatters */}
|
||||
<div className="absolute top-1/2 left-1/2 w-8 h-8 bg-primary/20 rounded-full blur-sm"></div>
|
||||
<div className="absolute top-1/4 right-1/3 w-6 h-6 bg-secondary/20 rounded-full blur-sm"></div>
|
||||
<div className="absolute bottom-1/4 left-1/3 w-4 h-4 bg-primary/15 rounded-full blur-sm"></div>
|
||||
<div className="absolute top-1/2 left-1/2 w-8 h-8 bg-warm-coral/20 rounded-full blur-sm"></div>
|
||||
<div className="absolute top-1/4 right-1/3 w-6 h-6 bg-warm-coral/20 rounded-full blur-sm"></div>
|
||||
<div className="absolute bottom-1/4 left-1/3 w-4 h-4 bg-warm-coral/15 rounded-full blur-sm"></div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
{/* Header */}
|
||||
<div className="text-left mb-12 md:mb-16">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<div className="inline-flex items-center gap-2 bg-gray-50 px-4 py-2 rounded-full mb-4">
|
||||
<Camera className="w-4 h-4 text-primary" />
|
||||
<Camera className="w-4 h-4 text-warm-coral" />
|
||||
<span className="text-sm text-gray-700">Custom Memories</span>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl xl:text-6xl text-gray-900 mb-6">
|
||||
<span className="font-light">How to create your</span>
|
||||
<span className="font-light">The Only Card That Sends Your</span>
|
||||
<span className="block">
|
||||
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic">
|
||||
custom
|
||||
<span className="font-bold text-warm-coral italic">
|
||||
Holiday
|
||||
</span>{' '}
|
||||
<span className="font-normal">postcard?</span>
|
||||
<span className="font-normal">Home.</span>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-lg md:text-xl text-gray-600 max-w-3xl leading-relaxed">
|
||||
Transform your travel experiences into beautiful, personalized postcards with authentic handwritten messages. Share your journey in a way that feels truly personal.
|
||||
<p className="text-lg md:text-xl text-gray-600 leading-relaxed max-w-3xl mx-auto">
|
||||
Transform your travel memories into beautiful, personalized postcards that capture the essence of your adventures.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Content - Responsive Grid */}
|
||||
<div className="grid lg:grid-cols-2 gap-8 lg:gap-16 xl:gap-20 items-center max-w-7xl mx-auto">
|
||||
|
||||
{/* Left Column - Interactive Postcard with 3D Effect */}
|
||||
{/* Centered Postcard Preview - Enhanced with Animations */}
|
||||
<div className="flex justify-center mb-12 px-4">
|
||||
<motion.div
|
||||
className="relative flex items-center justify-center order-2 lg:order-1"
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={() => setEditingCard(null)}
|
||||
className="relative group w-full max-w-4xl [perspective:2000px]"
|
||||
initial={{ opacity: 0, y: 30, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
delay: 0.2
|
||||
}}
|
||||
>
|
||||
{/* Main Postcard Container with 3D Transform */}
|
||||
{/* Interactive 3D Container with Flip */}
|
||||
<motion.div
|
||||
ref={postcardRef}
|
||||
className="relative w-full max-w-lg mx-auto"
|
||||
className="relative w-full rounded-xl cursor-pointer [transform-style:preserve-3d] transition-transform duration-700"
|
||||
style={{
|
||||
perspective: '1000px',
|
||||
aspectRatio: '1.6/1'
|
||||
aspectRatio: '720 / 470',
|
||||
maxWidth: '720px',
|
||||
maxHeight: '470px',
|
||||
minWidth: '280px',
|
||||
minHeight: '183px',
|
||||
rotateX,
|
||||
rotateY,
|
||||
transformStyle: "preserve-3d"
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(99, 102, 241, 0.1)",
|
||||
rotateY: 180,
|
||||
transition: { duration: 0.6 }
|
||||
}}
|
||||
animate={{
|
||||
y: [0, -8, 0],
|
||||
}}
|
||||
transition={{
|
||||
y: {
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Front Side - Postcard Design */}
|
||||
<motion.div
|
||||
className="w-full h-full"
|
||||
className="absolute inset-0 rounded-xl overflow-hidden [backface-visibility:hidden]"
|
||||
style={{
|
||||
rotateX,
|
||||
rotateY,
|
||||
transformStyle: 'preserve-3d'
|
||||
backfaceVisibility: "hidden",
|
||||
WebkitBackfaceVisibility: "hidden"
|
||||
}}
|
||||
>
|
||||
<PostcardFrame />
|
||||
|
||||
{/* Subtle glow effect */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-xl opacity-0 pointer-events-none"
|
||||
style={{
|
||||
background: "radial-gradient(circle at center, rgba(99, 102, 241, 0.1) 0%, transparent 70%)",
|
||||
filter: "blur(20px)"
|
||||
}}
|
||||
whileHover={{ opacity: 0.5 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Back Side - Full Image */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-xl overflow-hidden [backface-visibility:hidden]"
|
||||
style={{
|
||||
backfaceVisibility: "hidden",
|
||||
WebkitBackfaceVisibility: "hidden",
|
||||
transform: "rotateY(180deg)"
|
||||
}}
|
||||
>
|
||||
<ImageWithFallback
|
||||
src={postcardImage}
|
||||
alt="Postcard travel destination"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{/* Subtle gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-black/5 via-transparent to-black/10" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Floating Elements around postcard */}
|
||||
<motion.div
|
||||
className="absolute -top-6 -right-6 w-16 h-16 bg-primary/10 rounded-full blur-lg"
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.3, 0.6, 0.3]
|
||||
}}
|
||||
transition={{
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="absolute -bottom-8 -left-8 w-20 h-20 bg-secondary/10 rounded-full blur-lg"
|
||||
animate={{
|
||||
scale: [1.2, 1, 1.2],
|
||||
opacity: [0.4, 0.2, 0.4]
|
||||
}}
|
||||
transition={{
|
||||
duration: 5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Column - Features and CTA */}
|
||||
<div className="space-y-8 order-1 lg:order-2">
|
||||
|
||||
{/* Features List */}
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{
|
||||
icon: Camera,
|
||||
title: "Capture & Customize",
|
||||
description: "Upload your travel photos and customize every detail of your postcard."
|
||||
},
|
||||
{
|
||||
icon: Edit3,
|
||||
title: "Handwritten Messages",
|
||||
description: "Add personal messages that appear in beautiful handwritten script."
|
||||
},
|
||||
{
|
||||
icon: ArrowRight,
|
||||
title: "Share Instantly",
|
||||
description: "Send digital postcards or order physical prints delivered worldwide."
|
||||
}
|
||||
].map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="flex items-start gap-4 p-4 bg-gray-50/50 rounded-xl border border-gray-100/50 hover:border-primary/20 hover:bg-gray-50 transition-all duration-300"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-primary/10 to-secondary/10 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<feature.icon className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900 mb-2">{feature.title}</h4>
|
||||
<p className="text-gray-600 leading-relaxed">{feature.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA Section */}
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
onClick={handleCreatePostcard}
|
||||
size="xl"
|
||||
className="w-full md:w-auto h-14 px-8 rounded-xl bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 shadow-lg hover:shadow-xl transition-all duration-300 group"
|
||||
>
|
||||
<Camera className="w-5 h-5 mr-2 group-hover:scale-110 transition-transform" />
|
||||
Create Your Postcard
|
||||
<ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
|
||||
<p className="text-sm text-gray-500 leading-relaxed">
|
||||
Turn your travel memories into timeless keepsakes. Click anywhere on the postcard above to start customizing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Interactive Hint */}
|
||||
{!editingCard && (
|
||||
{/* Animated Edit Instructions */}
|
||||
{editingCard && (
|
||||
<motion.div
|
||||
className="flex items-center gap-2 text-sm text-primary/70 bg-primary/5 p-3 rounded-lg border border-primary/10"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 2 }}
|
||||
className="absolute -top-12 left-1/2 transform -translate-x-1/2 bg-primary text-white px-3 py-2 md:px-4 md:py-2 rounded-lg shadow-lg text-xs md:text-sm font-medium whitespace-nowrap z-20"
|
||||
initial={{ opacity: 0, y: 10, scale: 0.9 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -10, scale: 0.9 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.2, 1] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
<motion.span
|
||||
animate={{
|
||||
color: ["#ffffff", "#e0e7ff", "#ffffff"]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
>
|
||||
✨
|
||||
</motion.div>
|
||||
<span>Hover over the postcard and click any element to edit it!</span>
|
||||
Click on any element to edit it
|
||||
</motion.span>
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-primary"></div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Animated Editing Panel */}
|
||||
{editingCard && (
|
||||
<motion.div
|
||||
className="max-w-md mx-auto mb-12 p-6 bg-gray-50 rounded-lg border"
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -20, scale: 0.95 }}
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
>
|
||||
<h3 className="font-medium mb-4 capitalize">Edit {editingCard}</h3>
|
||||
{editingCard === 'photo' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Photo URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={postcardData.photo}
|
||||
onChange={(e) => setPostcardData(prev => ({ ...prev, photo: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Enter image URL"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{editingCard === 'message' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Message</label>
|
||||
<textarea
|
||||
value={postcardData.message}
|
||||
onChange={(e) => setPostcardData(prev => ({ ...prev, message: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary h-32 resize-none"
|
||||
placeholder="Enter your message"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{editingCard === 'date' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Date Text</label>
|
||||
<input
|
||||
type="text"
|
||||
value={postcardData.date}
|
||||
onChange={(e) => setPostcardData(prev => ({ ...prev, date: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Enter date text"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{editingCard === 'label' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Address Label</label>
|
||||
<input
|
||||
type="text"
|
||||
value={postcardData.addressLabel}
|
||||
onChange={(e) => setPostcardData(prev => ({ ...prev, addressLabel: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Enter label text"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{editingCard === 'stamp' && (
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Stamp design features authentic vintage styling with realistic aging effects.</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2 mt-4">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
onClick={() => setEditingCard(null)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Enhanced Call to Action */}
|
||||
<div className="text-center">
|
||||
<Button
|
||||
onClick={handleCreatePostcard}
|
||||
className="bg-warm-coral hover:bg-warm-coral/90 text-white px-8 py-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 mx-auto"
|
||||
>
|
||||
Customize Your Postcard
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</Button>
|
||||
<motion.p
|
||||
className="text-gray-600 mt-6"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.6 }}
|
||||
>
|
||||
Click on any postcard element above to edit it, or create a completely new design
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Features Section */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto mb-10 mt-12">
|
||||
{[
|
||||
{
|
||||
icon: Stamp,
|
||||
title: "Authentic Design",
|
||||
description: "Realistic vintage styling with aging effect, paper texture, and authentic details"
|
||||
},
|
||||
{
|
||||
icon: Edit,
|
||||
title: "Handwritten Style",
|
||||
description: "Beautiful cursive fonts with realistic ink bleeding and natural imperfections"
|
||||
},
|
||||
{
|
||||
icon: Camera,
|
||||
title: "Easy Customization",
|
||||
description: "Click any element to edit photos, messages, and details in real-time"
|
||||
}
|
||||
].map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="text-center group p-6 rounded-xl bg-white hover:bg-gray-50 transition-all duration-300 border border-gray-100 hover:border-warm-coral/20 hover:shadow-lg"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }}
|
||||
whileHover={{
|
||||
y: -5,
|
||||
transition: { duration: 0.2 }
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className="mx-auto mb-4 p-3 rounded-full bg-warm-coral/10 group-hover:bg-warm-coral/20 transition-all duration-300 w-16 h-16 flex items-center justify-center"
|
||||
whileHover={{
|
||||
scale: 1.1,
|
||||
rotate: 360,
|
||||
transition: { duration: 0.5 }
|
||||
}}
|
||||
>
|
||||
<feature.icon className="w-8 h-8 text-warm-coral" />
|
||||
</motion.div>
|
||||
<h3 className="mb-2 text-gray-900 group-hover:text-warm-coral transition-colors duration-300">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
339
src/components/LandingBookAttractionSection.tsx
Normal file
339
src/components/LandingBookAttractionSection.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Clock, Users, Star, Zap, CheckCircle, MapPin, Volume2, Camera } from 'lucide-react';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { motion } from 'motion/react';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
const attractions = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Sydney Opera House",
|
||||
city: "Sydney",
|
||||
country: "Australia",
|
||||
image: "https://images.unsplash.com/photo-1657622884558-cc7525f93638?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzeWRuZXklMjBvcGVyYSUyMGhvdXNlJTIwaGFyYm9yJTIwYnJpZGdlfGVufDF8fHx8MTc1NjExNDMwMHww&ixlib=rb-4.1.0&q=80&w=1080",
|
||||
rating: 4.8,
|
||||
reviews: "12,500+",
|
||||
category: "Landmarks",
|
||||
originalPrice: "$89",
|
||||
includedValue: "$89",
|
||||
perks: [
|
||||
{ icon: Zap, label: "Skip-the-line", color: "text-green-600" },
|
||||
{ icon: Volume2, label: "Audio guide", color: "text-blue-600" },
|
||||
{ icon: Camera, label: "Photo spots", color: "text-purple-600" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Great Ocean Road",
|
||||
city: "Melbourne",
|
||||
country: "Australia",
|
||||
image: "https://images.unsplash.com/photo-1557544780-585e99807b15?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZCUyMHR3ZWx2ZSUyMGFwb3N0bGVzfGVufDF8fHx8MTc1NjExNDMwNHww&ixlib=rb-4.1.0&q=80&w=1080",
|
||||
rating: 4.9,
|
||||
reviews: "8,200+",
|
||||
category: "Nature",
|
||||
originalPrice: "$125",
|
||||
includedValue: "$125",
|
||||
perks: [
|
||||
{ icon: Users, label: "Guided tour", color: "text-blue-600" },
|
||||
{ icon: MapPin, label: "Transport", color: "text-green-600" },
|
||||
{ icon: Camera, label: "Photo stops", color: "text-purple-600" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Lone Pine Koala Sanctuary",
|
||||
city: "Brisbane",
|
||||
country: "Australia",
|
||||
image: "https://images.unsplash.com/photo-1625476038303-0d3022077d39?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsb25lJTIwcGluZSUyMGtvYWxhJTIwc2FuY3R1YXJ5JTIwYnJpc2JhbmV8ZW58MXx8fHwxNzU2MTE0MzA3fDA&ixlib=rb-4.1.0&q=80&w=1080",
|
||||
rating: 4.7,
|
||||
reviews: "15,800+",
|
||||
category: "Wildlife",
|
||||
originalPrice: "$65",
|
||||
includedValue: "$65",
|
||||
perks: [
|
||||
{ icon: Zap, label: "Skip-the-line", color: "text-green-600" },
|
||||
{ icon: Users, label: "Animal encounters", color: "text-orange-600" },
|
||||
{ icon: Camera, label: "Photo opportunities", color: "text-purple-600" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Kings Park",
|
||||
city: "Perth",
|
||||
country: "Australia",
|
||||
image: "https://images.unsplash.com/photo-1667315682754-852d9e855207?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxraW5ncyUyMHBhcmslMjBwZXJ0aCUyMGJvdGFuaWNhbCUyMGdhcmRlbnxlbnwxfHx8fDE3NTYxMTQzMTJ8MA&ixlib=rb-4.1.0&q=80&w=1080",
|
||||
rating: 4.6,
|
||||
reviews: "9,400+",
|
||||
category: "Parks",
|
||||
originalPrice: "Free",
|
||||
includedValue: "$35",
|
||||
perks: [
|
||||
{ icon: Users, label: "Walking tours", color: "text-blue-600" },
|
||||
{ icon: Volume2, label: "Audio guide", color: "text-blue-600" },
|
||||
{ icon: MapPin, label: "Trail maps", color: "text-green-600" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Barossa Valley",
|
||||
city: "Adelaide",
|
||||
country: "Australia",
|
||||
image: "https://images.unsplash.com/photo-1578274821879-08e7f9050d83?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxiYXJvc3NhJTIwdmFsbGV5JTIwdmluZXlhcmQlMjB3aW5lcnl8ZW58MXx8fHwxNzU2MTE0MzE3fDA&ixlib=rb-4.1.0&q=80&w=1080",
|
||||
rating: 4.8,
|
||||
reviews: "6,700+",
|
||||
category: "Wine Tours",
|
||||
originalPrice: "$98",
|
||||
includedValue: "$98",
|
||||
perks: [
|
||||
{ icon: Users, label: "Wine tastings", color: "text-purple-600" },
|
||||
{ icon: MapPin, label: "Transport", color: "text-green-600" },
|
||||
{ icon: Volume2, label: "Expert guide", color: "text-blue-600" }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const categories = ["All", "Landmarks", "Nature", "Wildlife", "Parks", "Wine Tours"];
|
||||
|
||||
export function LandingBookAttractionSection() {
|
||||
const [activeCategory, setActiveCategory] = useState("All");
|
||||
|
||||
const filteredAttractions = activeCategory === "All"
|
||||
? attractions
|
||||
: attractions.filter(attraction => attraction.category === activeCategory);
|
||||
|
||||
const AttractionCard = ({ attraction, index }: { attraction: typeof attractions[0], index: number }) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
className="group cursor-pointer flex-shrink-0 w-[280px] md:w-auto md:flex-shrink h-96 flip-card-container"
|
||||
>
|
||||
{/* 3D Flip Container */}
|
||||
<div className="flip-card-inner group-hover:[transform:rotateY(180deg)] relative w-full h-full">
|
||||
|
||||
{/* FRONT FACE */}
|
||||
<div className="flip-card-face absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg">
|
||||
{/* Background Image */}
|
||||
<ImageWithFallback
|
||||
src={attraction.image}
|
||||
alt={attraction.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
||||
{/* Rating Badge */}
|
||||
{/* <div className="absolute top-4 right-4 bg-white/95 backdrop-blur-sm rounded-full px-3 py-1.5 flex items-center gap-1 shadow-lg z-10">
|
||||
<div className="w-4 h-4 bg-yellow-500 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-xs">★</span>
|
||||
</div>
|
||||
<span className="font-poppins text-sm font-medium text-foreground">{attraction.rating}</span>
|
||||
</div> */}
|
||||
|
||||
{/* Front Content - Clean Title & Location */}
|
||||
<div className="absolute bottom-0 left-0 right-0">
|
||||
<div className="bg-black/50 p-6">
|
||||
<h3 className="font-poppins text-lg md:text-xl leading-snug font-semibold text-white mb-1">{attraction.name}</h3>
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal text-white/90">
|
||||
{attraction.city}, {attraction.country}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BACK FACE */}
|
||||
<div className="flip-card-face flip-card-back absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg bg-gray-900">
|
||||
{/* Back Content Container */}
|
||||
<div className="relative w-full h-full p-6 flex flex-col justify-center text-white">
|
||||
|
||||
{/* Included Value Section */}
|
||||
<div className="mb-4">
|
||||
<div className="inline-flex items-center gap-2 bg-warm-coral text-white px-3 py-1.5 rounded-full text-sm font-medium mb-3">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span>Included Value</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold mb-1">{attraction.includedValue}</div>
|
||||
<p className="text-white/80 text-sm">
|
||||
{attraction.originalPrice === "Free"
|
||||
? "Premium access included"
|
||||
: "Save money with CityCard"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* What's Included List */}
|
||||
<div className="mb-4">
|
||||
<h4 className="font-semibold text-sm mb-3">What's Included:</h4>
|
||||
<div className="space-y-2">
|
||||
{attraction.perks.slice(0, 3).map((perk, perkIndex) => (
|
||||
<div key={perkIndex} className="flex items-center gap-3 text-white/90">
|
||||
<div className="w-6 h-6 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center">
|
||||
<perk.icon className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<span className="text-sm">{perk.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Duration & Meta Info */}
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-4 text-white/80 text-sm">
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>2-3 hours</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>All ages</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Features */}
|
||||
<div className="border-t border-white/20 pt-4">
|
||||
<div className="flex items-center justify-between text-white/80 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-3 h-3" />
|
||||
<span>Mobile ticket</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
<span>Instant confirmation</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative Elements */}
|
||||
<div className="absolute top-4 right-4 w-16 h-16 bg-primary/20 rounded-full blur-xl"></div>
|
||||
<div className="absolute bottom-4 left-4 w-12 h-12 bg-primary/15 rounded-full blur-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-gray-50 relative overflow-hidden">
|
||||
<div className="container mx-auto px-4">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 bg-primary/10 px-4 py-2 rounded-full mb-6">
|
||||
<div className="w-2 h-2 bg-primary rounded-full"></div>
|
||||
<span className="font-poppins text-sm font-medium text-primary">
|
||||
Must-See Destinations
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight text-foreground mb-4">
|
||||
<span className="font-bold text-primary italic">
|
||||
Top
|
||||
</span>{' '}
|
||||
<span className="font-normal">Attractions</span>
|
||||
</h2>
|
||||
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
|
||||
Discover Australia's most iconic attractions and hidden gems across Sydney, Melbourne, Brisbane, Perth, and Adelaide - all included with your CityCard
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Category Tabs */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex flex-wrap justify-center gap-3 mb-12"
|
||||
>
|
||||
{categories.map((category, index) => (
|
||||
<motion.button
|
||||
key={category}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
viewport={{ once: true }}
|
||||
onClick={() => setActiveCategory(category)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={`px-6 py-4 h-14 rounded-full font-medium transition-all duration-300 ${activeCategory === category
|
||||
? 'bg-warm-coral text-white shadow-xl shadow-warm-coral/25 ring-2 ring-warm-coral/20'
|
||||
: 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-warm-coral/20 hover:bg-white'
|
||||
}`}
|
||||
>
|
||||
{category}
|
||||
</motion.button>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Mobile Horizontal Carousel */}
|
||||
<div className="block md:hidden mb-8">
|
||||
<div className="relative">
|
||||
{/* Scroll Container */}
|
||||
<div className="flex gap-6 overflow-x-auto scrollbar-hide pb-4 px-4 -mx-4">
|
||||
{filteredAttractions.map((attraction, index) => (
|
||||
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Scroll Indicators */}
|
||||
<div className="flex justify-center mt-6 gap-2">
|
||||
{Array.from({ length: Math.ceil(filteredAttractions.length / 2) }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-2 h-2 rounded-full bg-gray-300"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile Hint Text */}
|
||||
<div className="text-center mt-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
Swipe to explore more attractions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Bento Grid */}
|
||||
<div className="hidden md:block w-full">
|
||||
{/* Top Row - 3 equal cards */}
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
{filteredAttractions.slice(0, 3).map((attraction, index) => (
|
||||
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Consistent Vertical Spacing */}
|
||||
<div className="h-6"></div>
|
||||
|
||||
{/* Bottom Row - 2 larger cards */}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{filteredAttractions.slice(3, 5).map((attraction, index) => (
|
||||
<AttractionCard key={attraction.id} attraction={attraction} index={index + 3} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Call to Action */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mt-12"
|
||||
>
|
||||
<Button
|
||||
withShine={true}
|
||||
size="xl"
|
||||
className="bg-primary hover:bg-primary/90 py-4 rounded-full text-lg font-poppins font-semibold px-8 text-white shadow-lg hover:shadow-xl transition-all duration-300"
|
||||
>
|
||||
Get Your City Card
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
937
src/components/LandingCustomPostcards.tsx
Normal file
937
src/components/LandingCustomPostcards.tsx
Normal file
@@ -0,0 +1,937 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Camera, ArrowRight, Edit3, Upload, Type, Calendar, Palette, Edit } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { motion, useMotionValue, useSpring, useTransform, useInView } from 'motion/react';
|
||||
import { HandwrittenText, useHandwrittenText } from './HandwrittenText';
|
||||
|
||||
interface EditableCardProps {
|
||||
isEditing: boolean;
|
||||
onEdit: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
editIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function LandingCustomPostcards() {
|
||||
const [editingCard, setEditingCard] = useState<string | null>(null);
|
||||
const postcardRef = useRef<HTMLDivElement>(null);
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 3D tilt effect using mouse position
|
||||
const mouseX = useMotionValue(0);
|
||||
const mouseY = useMotionValue(0);
|
||||
|
||||
// Spring animations for smooth mouse following
|
||||
const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [5, -5]), {
|
||||
stiffness: 100,
|
||||
damping: 15
|
||||
});
|
||||
const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-5, 5]), {
|
||||
stiffness: 100,
|
||||
damping: 15
|
||||
});
|
||||
|
||||
// Detect when section is in view to trigger handwriting
|
||||
const isInView = useInView(sectionRef, {
|
||||
once: true,
|
||||
margin: "-100px",
|
||||
amount: 0.3
|
||||
});
|
||||
|
||||
// Handwritten text control
|
||||
const handwrittenControl = useHandwrittenText(false);
|
||||
|
||||
// Handle mouse movement for 3D effect
|
||||
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!postcardRef.current) return;
|
||||
|
||||
const rect = postcardRef.current.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
|
||||
const x = (event.clientX - centerX) / (rect.width / 2);
|
||||
const y = (event.clientY - centerY) / (rect.height / 2);
|
||||
|
||||
mouseX.set(x);
|
||||
mouseY.set(y);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
mouseX.set(0);
|
||||
mouseY.set(0);
|
||||
};
|
||||
|
||||
const [postcardData, setPostcardData] = useState({
|
||||
photo: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=600&fit=crop&crop=center",
|
||||
message: "Greetings from paradise!\\nThe beaches here are absolutely\\nbreathtaking. Wish you were\\nhere to enjoy this amazing\\nsunset with me.",
|
||||
date: "July 2024",
|
||||
addressLabel: "POSTCARD"
|
||||
});
|
||||
|
||||
const handleCreatePostcard = () => {
|
||||
console.log('Navigate to postcard creation page...');
|
||||
};
|
||||
|
||||
// Start handwriting animation when section comes into view
|
||||
useEffect(() => {
|
||||
if (isInView && !editingCard) {
|
||||
// Delay the start of handwriting to let the postcard animation settle
|
||||
const timer = setTimeout(() => {
|
||||
handwrittenControl.start();
|
||||
}, 1200);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isInView, editingCard, handwrittenControl]);
|
||||
|
||||
// Reset handwriting when editing
|
||||
useEffect(() => {
|
||||
if (editingCard === 'message') {
|
||||
handwrittenControl.reset();
|
||||
}
|
||||
}, [editingCard, handwrittenControl]);
|
||||
|
||||
const EditableCard = ({ isEditing, onEdit, children, className = "", style = {}, editIcon }: EditableCardProps) => {
|
||||
return (
|
||||
<motion.div
|
||||
className={`relative group cursor-pointer ${className}`}
|
||||
style={style}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
transition: { duration: 0.2 }
|
||||
}}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
animate={isEditing ? {
|
||||
boxShadow: "0 0 0 2px rgba(249, 95, 98, 0.5), 0 8px 16px rgba(249, 95, 98, 0.3)"
|
||||
} : {}}
|
||||
>
|
||||
{children}
|
||||
|
||||
{/* Animated Edit overlay */}
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black rounded-md flex items-center justify-center"
|
||||
initial={{ opacity: 0, backgroundColor: "rgba(0, 0, 0, 0)" }}
|
||||
animate={isEditing ? {
|
||||
opacity: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.2)"
|
||||
} : {
|
||||
opacity: 0,
|
||||
backgroundColor: "rgba(0, 0, 0, 0)"
|
||||
}}
|
||||
whileHover={!isEditing ? {
|
||||
opacity: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)"
|
||||
} : {}}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
className="bg-white bg-opacity-90 px-2 py-1 rounded-md shadow-lg border border-gray-200 flex items-center gap-1"
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
animate={isEditing ? { rotate: [0, 10, -10, 0] } : {}}
|
||||
transition={{ duration: 2, repeat: isEditing ? Infinity : 0 }}
|
||||
>
|
||||
{editIcon}
|
||||
</motion.div>
|
||||
<motion.span
|
||||
className="text-xs text-gray-700 font-medium"
|
||||
animate={isEditing ? {
|
||||
color: ["#374151", "#F95F62", "#374151"]
|
||||
} : {}}
|
||||
transition={{ duration: 1, repeat: isEditing ? Infinity : 0 }}
|
||||
>
|
||||
{isEditing ? 'Editing...' : 'Edit'}
|
||||
</motion.span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
// Ultra-realistic vintage postcard with responsive scaling and animations
|
||||
const PostcardFrame = () => {
|
||||
return (
|
||||
<motion.div
|
||||
className="relative shadow-2xl transform rotate-[0.5deg] w-full h-full"
|
||||
initial={{ opacity: 0, scale: 0.9, rotate: 0 }}
|
||||
animate={{ opacity: 1, scale: 1, rotate: 0.5 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(circle at 20% 80%, rgba(210, 180, 140, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(160, 130, 100, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(190, 160, 120, 0.08) 0%, transparent 50%),
|
||||
linear-gradient(135deg, #f8f4e6 0%, #f0e7d3 25%, #ede2d0 50%, #e8dcc8 75%, #e3d5c2 100%)
|
||||
`,
|
||||
borderRadius: '10px',
|
||||
boxShadow: `
|
||||
inset 0px 1px 30px rgba(139, 115, 85, 0.2),
|
||||
inset 0px -1px 20px rgba(160, 130, 100, 0.15),
|
||||
0px 12px 40px rgba(0, 0, 0, 0.15),
|
||||
0px 4px 12px rgba(0, 0, 0, 0.1)
|
||||
`,
|
||||
border: '1px solid rgba(139, 115, 85, 0.2)'
|
||||
}}
|
||||
>
|
||||
{/* Realistic paper texture with variations */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-40 rounded-[10px] pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23d4af9a' fill-opacity='0.08'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"),
|
||||
radial-gradient(circle at 25% 75%, rgba(180, 150, 120, 0.1) 0%, transparent 40%),
|
||||
radial-gradient(circle at 75% 25%, rgba(160, 130, 100, 0.08) 0%, transparent 35%)
|
||||
`
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Age spots and stains */}
|
||||
<div className="absolute inset-0 rounded-[10px] pointer-events-none opacity-30">
|
||||
<div
|
||||
className="absolute w-8 h-6 rounded-full"
|
||||
style={{
|
||||
background: 'radial-gradient(ellipse, rgba(160, 120, 80, 0.15) 0%, transparent 70%)',
|
||||
top: '15%',
|
||||
right: '20%',
|
||||
transform: 'rotate(-15deg)'
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute w-6 h-8 rounded-full"
|
||||
style={{
|
||||
background: 'radial-gradient(ellipse, rgba(140, 110, 70, 0.12) 0%, transparent 60%)',
|
||||
bottom: '25%',
|
||||
left: '15%',
|
||||
transform: 'rotate(25deg)'
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute w-4 h-4 rounded-full"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(180, 140, 100, 0.2) 0%, transparent 50%)',
|
||||
top: '60%',
|
||||
right: '10%'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Corner wear and creases */}
|
||||
<div
|
||||
className="absolute top-0 right-0 w-12 h-12 opacity-25"
|
||||
style={{
|
||||
background: 'radial-gradient(circle at top right, rgba(139, 115, 85, 0.3) 20%, rgba(160, 130, 100, 0.2) 40%, transparent 70%)',
|
||||
borderTopRightRadius: '10px'
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 w-16 h-16 opacity-20"
|
||||
style={{
|
||||
background: 'radial-gradient(circle at bottom left, rgba(120, 95, 70, 0.25) 25%, rgba(140, 115, 85, 0.15) 50%, transparent 75%)',
|
||||
borderBottomLeftRadius: '10px'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Subtle crease lines */}
|
||||
<div
|
||||
className="absolute w-full h-px bg-gradient-to-r from-transparent via-amber-800/10 to-transparent opacity-40"
|
||||
style={{
|
||||
top: '35%',
|
||||
transform: 'rotate(-0.5deg)'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Animated Vintage Vector Logo - Top Right - Mobile Optimized */}
|
||||
<motion.div
|
||||
className="absolute opacity-60 pointer-events-none"
|
||||
style={{
|
||||
top: '4.3%',
|
||||
right: '3.5%',
|
||||
width: '7.5%',
|
||||
height: '11.5%',
|
||||
filter: 'sepia(30%) saturate(120%) hue-rotate(15deg) brightness(85%) contrast(110%)'
|
||||
}}
|
||||
animate={{
|
||||
rotate: [5, 7, 3, 5],
|
||||
scale: [1, 1.02, 0.98, 1]
|
||||
}}
|
||||
transition={{
|
||||
duration: 8,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
>
|
||||
<ImageWithFallback
|
||||
src="https://images.unsplash.com/photo-1702825328124-dab63d85490e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx2aW50YWdlJTIwbG9nbyUyMHZlY3RvciUyMHBvc3RhbCUyMHN0YW1wfGVufDF8fHx8MTc1ODk5MjExN3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
||||
alt="Vintage logo"
|
||||
className="w-full h-full object-contain"
|
||||
style={{
|
||||
mixBlendMode: 'multiply',
|
||||
opacity: 0.8
|
||||
}}
|
||||
/>
|
||||
{/* Subtle aging overlay for the vector */}
|
||||
<div
|
||||
className="absolute inset-0 rounded-full"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(circle at 30% 30%, rgba(160, 130, 90, 0.2) 0%, transparent 60%),
|
||||
radial-gradient(circle at 70% 70%, rgba(140, 110, 70, 0.15) 0%, transparent 50%)
|
||||
`,
|
||||
mixBlendMode: 'multiply'
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Editable Photo Card with realistic mounting - Responsive */}
|
||||
<EditableCard
|
||||
isEditing={editingCard === 'photo'}
|
||||
onEdit={() => setEditingCard(editingCard === 'photo' ? null : 'photo')}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '33.3%',
|
||||
height: '72.3%',
|
||||
left: '4.9%',
|
||||
top: '7.4%',
|
||||
transform: 'rotate(-0.8deg)'
|
||||
}}
|
||||
editIcon={<Upload className="w-3 h-3 text-gray-600" />}
|
||||
>
|
||||
<div
|
||||
className="w-full h-full rounded-lg overflow-hidden shadow-lg relative"
|
||||
style={{
|
||||
border: '4px solid #ffffff',
|
||||
boxShadow: `
|
||||
0px 6px 25px rgba(0, 0, 0, 0.25),
|
||||
inset 0px 1px 3px rgba(255, 255, 255, 0.8),
|
||||
inset 0px -1px 2px rgba(160, 130, 100, 0.2)
|
||||
`
|
||||
}}
|
||||
>
|
||||
<ImageWithFallback
|
||||
src={postcardData.photo}
|
||||
alt="Beautiful tropical beach"
|
||||
className="w-full h-full object-cover"
|
||||
style={{
|
||||
filter: 'sepia(8%) saturate(110%) contrast(102%) brightness(98%)'
|
||||
}}
|
||||
/>
|
||||
{/* Photo aging overlay */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(circle at 20% 20%, rgba(255, 235, 205, 0.15) 0%, transparent 40%),
|
||||
radial-gradient(circle at 80% 80%, rgba(200, 170, 130, 0.1) 0%, transparent 35%)
|
||||
`,
|
||||
mixBlendMode: 'multiply'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</EditableCard>
|
||||
|
||||
{/* Realistic vertical divider with ink bleeding - Responsive */}
|
||||
<div
|
||||
className="absolute"
|
||||
style={{
|
||||
top: '10.6%',
|
||||
bottom: '10.6%',
|
||||
left: '43.1%',
|
||||
width: '2px',
|
||||
background: `
|
||||
linear-gradient(to bottom,
|
||||
transparent 0%,
|
||||
rgba(101, 84, 63, 0.4) 10%,
|
||||
rgba(101, 84, 63, 0.6) 30%,
|
||||
rgba(101, 84, 63, 0.8) 50%,
|
||||
rgba(101, 84, 63, 0.6) 70%,
|
||||
rgba(101, 84, 63, 0.4) 90%,
|
||||
transparent 100%
|
||||
)
|
||||
`,
|
||||
filter: 'blur(0.3px)'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Ink bleed effect around divider - Responsive */}
|
||||
<div
|
||||
className="absolute opacity-20"
|
||||
style={{
|
||||
top: '10.6%',
|
||||
bottom: '10.6%',
|
||||
left: '42.8%',
|
||||
width: '6px',
|
||||
background: `
|
||||
linear-gradient(to bottom,
|
||||
transparent 0%,
|
||||
rgba(101, 84, 63, 0.1) 20%,
|
||||
rgba(101, 84, 63, 0.15) 50%,
|
||||
rgba(101, 84, 63, 0.1) 80%,
|
||||
transparent 100%
|
||||
)
|
||||
`,
|
||||
filter: 'blur(1px)'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Right side content area - Responsive */}
|
||||
<div
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
top: '10.6%',
|
||||
left: '47.2%',
|
||||
width: '47.2%',
|
||||
height: '78.7%'
|
||||
}}
|
||||
>
|
||||
{/* Editable Address Label Card - Responsive */}
|
||||
<EditableCard
|
||||
isEditing={editingCard === 'label'}
|
||||
onEdit={() => setEditingCard(editingCard === 'label' ? null : 'label')}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '4.1%',
|
||||
left: '0%',
|
||||
pointerEvents: 'auto',
|
||||
transform: 'rotate(-0.3deg)'
|
||||
}}
|
||||
editIcon={<Type className="w-3 h-3 text-gray-600" />}
|
||||
>
|
||||
<div
|
||||
className="px-2 py-1 rounded text-xs md:text-sm"
|
||||
style={{
|
||||
fontWeight: '600',
|
||||
letterSpacing: '2px',
|
||||
textTransform: 'uppercase',
|
||||
color: 'rgba(101, 84, 63, 0.8)',
|
||||
textShadow: '0px 1px 2px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
>
|
||||
{postcardData.addressLabel}
|
||||
</div>
|
||||
</EditableCard>
|
||||
|
||||
{/* Realistic horizontal ruled lines with ink bleeding - Responsive */}
|
||||
<div
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
top: '14.9%',
|
||||
left: '0%',
|
||||
width: '94.1%',
|
||||
height: '59.5%'
|
||||
}}
|
||||
>
|
||||
{[...Array(9)].map((_, i) => (
|
||||
<div key={i} className="relative" style={{ marginBottom: '6.5%' }}>
|
||||
{/* Main line */}
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
width: i % 2 === 0 ? '100%' : '92%',
|
||||
background: `linear-gradient(to right,
|
||||
rgba(101, 84, 63, 0.3) 0%,
|
||||
rgba(101, 84, 63, 0.5) 20%,
|
||||
rgba(101, 84, 63, 0.6) 50%,
|
||||
rgba(101, 84, 63, 0.4) 80%,
|
||||
rgba(101, 84, 63, 0.2) 95%,
|
||||
transparent 100%
|
||||
)`,
|
||||
transform: `rotate(${(Math.random() - 0.5) * 0.5}deg)`
|
||||
}}
|
||||
/>
|
||||
{/* Ink bleed effect */}
|
||||
<div
|
||||
className="absolute top-0 left-0 opacity-30"
|
||||
style={{
|
||||
height: '3px',
|
||||
width: i % 2 === 0 ? '98%' : '90%',
|
||||
background: `linear-gradient(to right,
|
||||
rgba(101, 84, 63, 0.1) 0%,
|
||||
rgba(101, 84, 63, 0.2) 30%,
|
||||
rgba(101, 84, 63, 0.15) 70%,
|
||||
transparent 100%
|
||||
)`,
|
||||
filter: 'blur(1px)',
|
||||
transform: 'translateY(-1px)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Editable Message Card with Handwritten Animation - Responsive */}
|
||||
<EditableCard
|
||||
isEditing={editingCard === 'message'}
|
||||
onEdit={() => setEditingCard(editingCard === 'message' ? null : 'message')}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '17.6%',
|
||||
left: '4.4%',
|
||||
width: '88.2%',
|
||||
pointerEvents: 'auto',
|
||||
transform: 'rotate(-0.7deg)'
|
||||
}}
|
||||
editIcon={<Edit3 className="w-3 h-3 text-gray-600" />}
|
||||
>
|
||||
<div className="px-2 py-2 rounded leading-relaxed">
|
||||
{editingCard === 'message' ? (
|
||||
// Show static text when editing
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'Dancing Script', 'Brush Script MT', cursive",
|
||||
fontSize: 'clamp(14px, 3.6vw, 26px)',
|
||||
lineHeight: '1.7',
|
||||
color: 'rgba(85, 70, 50, 0.9)',
|
||||
textShadow: `
|
||||
1px 1px 2px rgba(0, 0, 0, 0.08),
|
||||
0px 0px 3px rgba(101, 84, 63, 0.1)
|
||||
`,
|
||||
whiteSpace: 'pre-line',
|
||||
filter: 'contrast(110%) brightness(98%)'
|
||||
}}
|
||||
>
|
||||
{postcardData.message}
|
||||
</div>
|
||||
) : (
|
||||
// Show animated handwritten text when not editing
|
||||
<HandwrittenText
|
||||
text={postcardData.message}
|
||||
speed={6}
|
||||
startDelay={0}
|
||||
autoStart={false}
|
||||
onComplete={handwrittenControl.onComplete}
|
||||
style={{
|
||||
fontFamily: "'Dancing Script', 'Brush Script MT', cursive",
|
||||
fontSize: 'clamp(14px, 3.6vw, 26px)',
|
||||
lineHeight: '1.7',
|
||||
color: 'rgba(85, 70, 50, 0.9)',
|
||||
textShadow: `
|
||||
1px 1px 2px rgba(0, 0, 0, 0.08),
|
||||
0px 0px 3px rgba(101, 84, 63, 0.1)
|
||||
`,
|
||||
filter: 'contrast(110%) brightness(98%)'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</EditableCard>
|
||||
|
||||
{/* Editable Date Card - Responsive */}
|
||||
<EditableCard
|
||||
isEditing={editingCard === 'date'}
|
||||
onEdit={() => setEditingCard(editingCard === 'date' ? null : 'date')}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '16.2%',
|
||||
right: '7.4%',
|
||||
pointerEvents: 'auto',
|
||||
transform: 'rotate(-1.5deg)'
|
||||
}}
|
||||
editIcon={<Calendar className="w-3 h-3 text-gray-600" />}
|
||||
>
|
||||
<div
|
||||
className="px-2 py-1 rounded text-xs"
|
||||
style={{
|
||||
fontWeight: '500',
|
||||
letterSpacing: '1px',
|
||||
color: 'rgba(101, 84, 63, 0.7)',
|
||||
textShadow: '0px 1px 1px rgba(0, 0, 0, 0.05)',
|
||||
fontFamily: "'Poppins', sans-serif"
|
||||
}}
|
||||
>
|
||||
{postcardData.date}
|
||||
</div>
|
||||
</EditableCard>
|
||||
</div>
|
||||
|
||||
{/* Ultra-realistic stamp - Responsive */}
|
||||
<EditableCard
|
||||
isEditing={editingCard === 'stamp'}
|
||||
onEdit={() => setEditingCard(editingCard === 'stamp' ? null : 'stamp')}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '12.5%',
|
||||
height: '19.1%',
|
||||
right: '3.5%',
|
||||
bottom: '5.3%',
|
||||
transform: 'rotate(-12deg)'
|
||||
}}
|
||||
editIcon={<Edit3 className="w-3 h-3 text-gray-600" />}
|
||||
>
|
||||
<div
|
||||
className="w-full h-full border-2 border-dashed rounded-full flex items-center justify-center relative"
|
||||
style={{
|
||||
borderColor: 'rgba(139, 115, 85, 0.8)',
|
||||
background: `
|
||||
radial-gradient(circle at 30% 30%, rgba(220, 190, 150, 0.95) 0%, rgba(200, 170, 130, 0.9) 40%, rgba(180, 150, 110, 0.85) 100%),
|
||||
linear-gradient(135deg, rgba(240, 220, 180, 0.3) 0%, transparent 50%)
|
||||
`,
|
||||
boxShadow: `
|
||||
inset 0px 2px 8px rgba(0, 0, 0, 0.25),
|
||||
inset 0px -1px 4px rgba(255, 255, 255, 0.2),
|
||||
0px 4px 12px rgba(0, 0, 0, 0.15)
|
||||
`
|
||||
}}
|
||||
>
|
||||
{/* Stamp aging and wear */}
|
||||
<div
|
||||
className="absolute inset-0 rounded-full pointer-events-none"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(circle at 70% 20%, rgba(160, 130, 90, 0.3) 0%, transparent 40%),
|
||||
radial-gradient(circle at 20% 80%, rgba(140, 110, 70, 0.2) 0%, transparent 35%)
|
||||
`,
|
||||
mixBlendMode: 'multiply'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Stamp inner details */}
|
||||
<div className="absolute inset-3 border border-amber-800/50 rounded-full" />
|
||||
<div className="absolute inset-5 border border-amber-800/30 rounded-full" />
|
||||
|
||||
{/* Stamp text */}
|
||||
<div className="text-center leading-tight z-10" style={{ color: 'rgba(101, 84, 63, 0.9)' }}>
|
||||
<div style={{ fontSize: 'clamp(6px, 1.25vw, 9px)', fontWeight: '700', letterSpacing: '1px' }}>
|
||||
TRAVEL
|
||||
</div>
|
||||
<div style={{ fontSize: 'clamp(5px, 0.97vw, 7px)', fontWeight: '600', marginTop: '2px', letterSpacing: '0.5px' }}>
|
||||
MEMORIES
|
||||
</div>
|
||||
<div style={{ fontSize: 'clamp(5px, 0.97vw, 7px)', fontWeight: '500', marginTop: '1px' }}>
|
||||
2024
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stamp perforations with realistic variations */}
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute rounded-full"
|
||||
style={{
|
||||
width: '2px',
|
||||
height: '2px',
|
||||
backgroundColor: 'rgba(101, 84, 63, 0.4)',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: `translate(-50%, -50%) rotate(${i * 18}deg) translateY(-42px)`,
|
||||
opacity: Math.random() > 0.1 ? 1 : 0.3 // Random missing perforations
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Stamp smudge mark */}
|
||||
<div
|
||||
className="absolute w-3 h-2 opacity-25"
|
||||
style={{
|
||||
background: 'radial-gradient(ellipse, rgba(101, 84, 63, 0.4) 0%, transparent 70%)',
|
||||
bottom: '15%',
|
||||
right: '20%',
|
||||
transform: 'rotate(20deg)',
|
||||
filter: 'blur(0.5px)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</EditableCard>
|
||||
|
||||
{/* Additional realistic aging effects */}
|
||||
<div
|
||||
className="absolute w-3 h-1 opacity-15"
|
||||
style={{
|
||||
background: 'linear-gradient(45deg, rgba(120, 100, 70, 0.3), transparent)',
|
||||
top: '20%',
|
||||
left: '60%',
|
||||
transform: 'rotate(45deg)',
|
||||
filter: 'blur(0.5px)'
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="py-12 md:py-16 lg:py-20 bg-white relative overflow-hidden">
|
||||
{/* Background decorations */}
|
||||
<div className="absolute inset-0 opacity-10 overflow-hidden">
|
||||
{/* Vintage Stamps */}
|
||||
<div className="absolute top-20 left-20 w-16 h-20 bg-warm-coral/30 rounded-sm rotate-12 border-2 border-warm-coral/20"></div>
|
||||
<div className="absolute top-40 right-32 w-12 h-16 bg-warm-coral/30 rounded-sm -rotate-6 border-2 border-warm-coral/20"></div>
|
||||
<div className="absolute bottom-32 left-40 w-14 h-18 bg-warm-coral/20 rounded-sm rotate-45 border-2 border-warm-coral/15"></div>
|
||||
|
||||
{/* Paper Textures */}
|
||||
<div className="absolute top-1/3 right-1/4 w-32 h-32 bg-warm-coral/10 rounded-full blur-2xl"></div>
|
||||
<div className="absolute bottom-1/3 left-1/4 w-40 h-40 bg-warm-coral/10 rounded-full blur-2xl"></div>
|
||||
|
||||
{/* Ink Splatters */}
|
||||
<div className="absolute top-1/2 left-1/2 w-8 h-8 bg-warm-coral/20 rounded-full blur-sm"></div>
|
||||
<div className="absolute top-1/4 right-1/3 w-6 h-6 bg-warm-coral/20 rounded-full blur-sm"></div>
|
||||
<div className="absolute bottom-1/4 left-1/3 w-4 h-4 bg-warm-coral/15 rounded-full blur-sm"></div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<div className="inline-flex items-center gap-2 bg-gray-50 px-4 py-2 rounded-full mb-4">
|
||||
<Camera className="w-4 h-4 text-warm-coral" />
|
||||
<span className="text-sm text-gray-700">Custom Memories</span>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl xl:text-6xl text-gray-900 mb-6">
|
||||
<span className="font-light">The Only Card That Sends Your</span>
|
||||
<span className="block">
|
||||
<span className="font-bold text-warm-coral italic">
|
||||
Holiday
|
||||
</span>{' '}
|
||||
<span className="font-normal">Home.</span>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-lg md:text-xl text-gray-600 leading-relaxed max-w-3xl mx-auto">
|
||||
Transform your travel memories into beautiful, personalized postcards that capture the essence of your adventures.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Centered Postcard Preview - Enhanced with Animations */}
|
||||
<div className="flex justify-center mb-12 px-4">
|
||||
<motion.div
|
||||
className="relative group w-full max-w-4xl"
|
||||
initial={{ opacity: 0, y: 30, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
delay: 0.2
|
||||
}}
|
||||
>
|
||||
{/* Interactive 3D Container */}
|
||||
<motion.div
|
||||
ref={postcardRef}
|
||||
className="relative w-full overflow-hidden rounded-xl cursor-pointer"
|
||||
style={{
|
||||
aspectRatio: '720 / 470',
|
||||
maxWidth: '720px',
|
||||
maxHeight: '470px',
|
||||
minWidth: '280px',
|
||||
minHeight: '183px',
|
||||
rotateX,
|
||||
rotateY,
|
||||
transformStyle: "preserve-3d"
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(99, 102, 241, 0.1)",
|
||||
transition: { duration: 0.3 }
|
||||
}}
|
||||
animate={{
|
||||
y: [0, -8, 0],
|
||||
}}
|
||||
transition={{
|
||||
y: {
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PostcardFrame />
|
||||
|
||||
{/* Subtle glow effect */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-xl opacity-0 pointer-events-none"
|
||||
style={{
|
||||
background: "radial-gradient(circle at center, rgba(99, 102, 241, 0.1) 0%, transparent 70%)",
|
||||
filter: "blur(20px)"
|
||||
}}
|
||||
whileHover={{ opacity: 0.5 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Animated Edit Instructions */}
|
||||
{editingCard && (
|
||||
<motion.div
|
||||
className="absolute -top-12 left-1/2 transform -translate-x-1/2 bg-primary text-white px-3 py-2 md:px-4 md:py-2 rounded-lg shadow-lg text-xs md:text-sm font-medium whitespace-nowrap z-20"
|
||||
initial={{ opacity: 0, y: 10, scale: 0.9 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -10, scale: 0.9 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<motion.span
|
||||
animate={{
|
||||
color: ["#ffffff", "#e0e7ff", "#ffffff"]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
>
|
||||
Click on any element to edit it
|
||||
</motion.span>
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-primary"></div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Animated Editing Panel */}
|
||||
{editingCard && (
|
||||
<motion.div
|
||||
className="max-w-md mx-auto mb-12 p-6 bg-gray-50 rounded-lg border"
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -20, scale: 0.95 }}
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
>
|
||||
<h3 className="font-medium mb-4 capitalize">Edit {editingCard}</h3>
|
||||
{editingCard === 'photo' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Photo URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={postcardData.photo}
|
||||
onChange={(e) => setPostcardData(prev => ({ ...prev, photo: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Enter image URL"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{editingCard === 'message' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Message</label>
|
||||
<textarea
|
||||
value={postcardData.message}
|
||||
onChange={(e) => setPostcardData(prev => ({ ...prev, message: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary h-32 resize-none"
|
||||
placeholder="Enter your message"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{editingCard === 'date' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Date Text</label>
|
||||
<input
|
||||
type="text"
|
||||
value={postcardData.date}
|
||||
onChange={(e) => setPostcardData(prev => ({ ...prev, date: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Enter date text"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{editingCard === 'label' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Address Label</label>
|
||||
<input
|
||||
type="text"
|
||||
value={postcardData.addressLabel}
|
||||
onChange={(e) => setPostcardData(prev => ({ ...prev, addressLabel: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Enter label text"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{editingCard === 'stamp' && (
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Stamp design features authentic vintage styling with realistic aging effects.</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2 mt-4">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
onClick={() => setEditingCard(null)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Enhanced Call to Action */}
|
||||
<div className="text-center">
|
||||
<Button
|
||||
onClick={handleCreatePostcard}
|
||||
className="bg-primary hover:bg-primary/90 text-white px-8 py-3 rounded-full shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 mx-auto mb-4"
|
||||
>
|
||||
Customize Your Postcard
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</Button>
|
||||
<motion.p
|
||||
className="text-gray-600"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.6 }}
|
||||
>
|
||||
Click on any postcard element above to edit it, or create a completely new design
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Features Section */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto mb-10 mt-2">
|
||||
{[
|
||||
{
|
||||
icon: Palette,
|
||||
title: "Authentic Design",
|
||||
description: "Realistic vintage styling with aging effect, paper texture, and authentic details"
|
||||
},
|
||||
{
|
||||
icon: Edit,
|
||||
title: "Handwritten Style",
|
||||
description: "Beautiful cursive fonts with realistic ink bleeding and natural imperfections"
|
||||
},
|
||||
{
|
||||
icon: Camera,
|
||||
title: "Easy Customization",
|
||||
description: "Click any element to edit photos, messages, and details in real-time"
|
||||
}
|
||||
].map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="text-center group p-6 rounded-xl bg-white hover:bg-gray-50 transition-all duration-300 border border-gray-100 hover:border-warm-coral/20 hover:shadow-lg"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }}
|
||||
whileHover={{
|
||||
y: -5,
|
||||
transition: { duration: 0.2 }
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className="mx-auto mb-4 p-3 rounded-full bg-warm-coral/10 group-hover:bg-warm-coral/20 transition-all duration-300 w-16 h-16 flex items-center justify-center"
|
||||
whileHover={{
|
||||
scale: 1.1,
|
||||
rotate: 360,
|
||||
transition: { duration: 0.5 }
|
||||
}}
|
||||
>
|
||||
<feature.icon className="w-8 h-8 text-warm-coral" />
|
||||
</motion.div>
|
||||
<h3 className="mb-2 text-gray-900 group-hover:text-warm-coral transition-colors duration-300">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
632
src/components/LandingMagicItinerary.tsx
Normal file
632
src/components/LandingMagicItinerary.tsx
Normal file
@@ -0,0 +1,632 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { Wand2, MapPin, Clock, DollarSign, Sparkles, Star, Navigation, Plane, Map } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
|
||||
interface ItineraryCard {
|
||||
id: number;
|
||||
city: string;
|
||||
country: string;
|
||||
days: number;
|
||||
image: string;
|
||||
activities: {
|
||||
time: string;
|
||||
name: string;
|
||||
price: string;
|
||||
}[];
|
||||
totalCost: string;
|
||||
highlights: string[];
|
||||
}
|
||||
|
||||
const itineraryCards: ItineraryCard[] = [
|
||||
{
|
||||
id: 1,
|
||||
city: 'Paris',
|
||||
country: 'France',
|
||||
days: 3,
|
||||
image: 'https://images.unsplash.com/photo-1431274172761-fca41d930114?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxQYXJpcyUyMEVpZmZlbCUyMFRvd2VyfGVufDF8fHx8MTc1OTIzNTg5MXww&ixlib=rb-4.1.0&q=80&w=1080',
|
||||
activities: [
|
||||
{ time: '9:00 AM', name: 'Eiffel Tower', price: '$28' },
|
||||
{ time: '1:00 PM', name: 'Louvre Museum', price: '$18' },
|
||||
{ time: '6:00 PM', name: 'Seine River Cruise', price: '$22' },
|
||||
],
|
||||
totalCost: '$68',
|
||||
highlights: ['Art & Culture', 'Historic Sites', 'Fine Dining'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
city: 'Tokyo',
|
||||
country: 'Japan',
|
||||
days: 4,
|
||||
image: 'https://images.unsplash.com/photo-1717986439981-0c6a51130cfa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxUb2t5byUyMGNpdHklMjBza3lsaW5lfGVufDF8fHx8MTc1OTI5Nzc3NHww&ixlib=rb-4.1.0&q=80&w=1080',
|
||||
activities: [
|
||||
{ time: '8:00 AM', name: 'Senso-ji Temple', price: 'Free' },
|
||||
{ time: '12:00 PM', name: 'Tokyo Skytree', price: '$24' },
|
||||
{ time: '5:00 PM', name: 'Shibuya Crossing', price: 'Free' },
|
||||
],
|
||||
totalCost: '$24',
|
||||
highlights: ['Modern Culture', 'Temples', 'Street Food'],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
city: 'New York',
|
||||
country: 'USA',
|
||||
days: 3,
|
||||
image: 'https://images.unsplash.com/photo-1698066574628-3d1a68c2f204?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxOZXclMjBZb3JrJTIwQ2l0eSUyME1hbmhhdHRhbnxlbnwxfHx8fDE3NTkyOTc3NzR8MA&ixlib=rb-4.1.0&q=80&w=1080',
|
||||
activities: [
|
||||
{ time: '9:00 AM', name: 'Central Park', price: 'Free' },
|
||||
{ time: '2:00 PM', name: 'Empire State Building', price: '$44' },
|
||||
{ time: '7:00 PM', name: 'Times Square', price: 'Free' },
|
||||
],
|
||||
totalCost: '$44',
|
||||
highlights: ['Urban Adventure', 'Skyscrapers', 'Broadway'],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
city: 'London',
|
||||
country: 'UK',
|
||||
days: 4,
|
||||
image: 'https://images.unsplash.com/photo-1745016176874-cd3ed3f5bfc6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxMb25kb24lMjBCaWclMjBCZW58ZW58MXx8fHwxNzU5Mjk3Nzc1fDA&ixlib=rb-4.1.0&q=80&w=1080',
|
||||
activities: [
|
||||
{ time: '10:00 AM', name: 'Tower of London', price: '$35' },
|
||||
{ time: '2:00 PM', name: 'British Museum', price: 'Free' },
|
||||
{ time: '6:00 PM', name: 'London Eye', price: '$32' },
|
||||
],
|
||||
totalCost: '$67',
|
||||
highlights: ['Royal Heritage', 'Museums', 'Theatre'],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
city: 'Barcelona',
|
||||
country: 'Spain',
|
||||
days: 3,
|
||||
image: 'https://images.unsplash.com/photo-1653677903266-1d814985b3cc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxCYXJjZWxvbmElMjBhcmNoaXRlY3R1cmV8ZW58MXx8fHwxNzU5Mjk3Nzc2fDA&ixlib=rb-4.1.0&q=80&w=1080',
|
||||
activities: [
|
||||
{ time: '9:30 AM', name: 'Sagrada Familia', price: '$26' },
|
||||
{ time: '1:00 PM', name: 'Park Güell', price: '$14' },
|
||||
{ time: '5:00 PM', name: 'La Rambla', price: 'Free' },
|
||||
],
|
||||
totalCost: '$40',
|
||||
highlights: ['Gaudí Architecture', 'Beach', 'Tapas'],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
city: 'Dubai',
|
||||
country: 'UAE',
|
||||
days: 3,
|
||||
image: 'https://images.unsplash.com/photo-1537132766573-55e8b870c5d6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxEdWJhaSUyMGNpdHlzY2FwZXxlbnwxfHx8fDE3NTkyOTc3NzV8MA&ixlib=rb-4.1.0&q=80&w=1080',
|
||||
activities: [
|
||||
{ time: '10:00 AM', name: 'Burj Khalifa', price: '$45' },
|
||||
{ time: '3:00 PM', name: 'Dubai Mall', price: 'Free' },
|
||||
{ time: '7:00 PM', name: 'Desert Safari', price: '$75' },
|
||||
],
|
||||
totalCost: '$120',
|
||||
highlights: ['Luxury', 'Desert Adventure', 'Shopping'],
|
||||
},
|
||||
];
|
||||
|
||||
export function LandingMagicItinerary() {
|
||||
const [currentCardIndex, setCurrentCardIndex] = useState(0);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setIsAnimating(true);
|
||||
setTimeout(() => {
|
||||
setCurrentCardIndex((prev) => (prev + 1) % itineraryCards.length);
|
||||
setIsAnimating(false);
|
||||
}, 400); // Half of the animation duration
|
||||
}, 4000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const currentCard = itineraryCards[currentCardIndex];
|
||||
const nextCard = itineraryCards[(currentCardIndex + 1) % itineraryCards.length];
|
||||
const thirdCard = itineraryCards[(currentCardIndex + 2) % itineraryCards.length];
|
||||
|
||||
return (
|
||||
<section className="relative py-20 lg:py-32 overflow-hidden -mt-20 pt-32 z-[100]">
|
||||
{/* Dynamic City Background */}
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentCard.id}
|
||||
className="absolute inset-0 overflow-hidden pointer-events-none z-[5]"
|
||||
initial={{ opacity: 0, scale: 1.05 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 1.05 }}
|
||||
transition={{ duration: 0.8, ease: "easeInOut" }}
|
||||
>
|
||||
{/* City Background Image */}
|
||||
<div className="absolute inset-0">
|
||||
<ImageWithFallback
|
||||
src={currentCard.image}
|
||||
alt={`${currentCard.city} background`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Lightened Multi-layer Gradient Overlay - Reduced opacity to show city */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-rose-50/50 via-orange-50/40 to-amber-50/50" />
|
||||
<div className="absolute inset-0 backdrop-blur-lg" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-white/30 via-transparent to-white/30" />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* White Readability Overlay - 42% Opacity */}
|
||||
<div className="absolute inset-0 bg-white/42 pointer-events-none z-[10]" />
|
||||
|
||||
{/* Simplified Decorative Elements - Optimized */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
{/* Single Coral Gradient Blob - Optimized */}
|
||||
<motion.div
|
||||
className="absolute top-20 -left-20 w-[500px] h-[500px] bg-gradient-to-br from-warm-coral/20 via-orange-400/10 to-transparent rounded-full blur-3xl will-change-transform"
|
||||
animate={{
|
||||
scale: [1, 1.15, 1],
|
||||
x: [0, 30, 0],
|
||||
}}
|
||||
transition={{ duration: 15, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
{/* Simplified Floating Icons - Reduced from 8 to 4 */}
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="absolute will-change-transform"
|
||||
style={{
|
||||
top: `${25 + (i * 20)}%`,
|
||||
left: `${15 + (i * 20)}%`,
|
||||
}}
|
||||
animate={{
|
||||
y: [0, -20, 0],
|
||||
opacity: [0.15, 0.35, 0.15],
|
||||
}}
|
||||
transition={{
|
||||
duration: 6 + i * 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: i * 0.8,
|
||||
}}
|
||||
>
|
||||
{i % 2 === 0 ? (
|
||||
<Plane className="w-6 h-6 text-warm-coral" />
|
||||
) : (
|
||||
<MapPin className="w-6 h-6 text-warm-coral" />
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-[101] flex flex-col items-center">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-20 max-w-5xl w-full">
|
||||
<motion.div
|
||||
className="inline-flex items-center gap-3 bg-gradient-to-r from-warm-coral/10 to-orange-100/50 backdrop-blur-sm px-6 py-3 rounded-full border-2 border-warm-coral/30 shadow-xl mb-8"
|
||||
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, ease: [0.34, 1.56, 0.64, 1] }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<motion.div
|
||||
animate={{
|
||||
rotate: [0, 20, -20, 0],
|
||||
scale: [1, 1.2, 1.2, 1],
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
>
|
||||
<Wand2 className="w-6 h-6 text-warm-coral drop-shadow-lg" />
|
||||
</motion.div>
|
||||
<span className="font-semibold text-gray-800">AI-Powered Magic Itinerary</span>
|
||||
<motion.div
|
||||
className="w-2 h-2 bg-warm-coral rounded-full"
|
||||
animate={{
|
||||
scale: [1, 1.5, 1],
|
||||
opacity: [1, 0.5, 1],
|
||||
}}
|
||||
transition={{ duration: 1.5, repeat: Infinity }}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.h2
|
||||
className="text-4xl md:text-5xl lg:text-6xl mb-8 leading-tight"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.1, ease: [0.34, 1.56, 0.64, 1] }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<span className="font-light">Plan Your</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-warm-coral via-orange-500 to-rose-500 bg-clip-text text-transparent drop-shadow-lg">
|
||||
Dream Journey
|
||||
</span>
|
||||
<br />
|
||||
<span className="font-normal">in Just</span>{' '}
|
||||
<span className="font-bold text-warm-coral">3 Seconds</span>
|
||||
<motion.span
|
||||
className="inline-block ml-2"
|
||||
animate={{
|
||||
rotate: [0, 10, -10, 0],
|
||||
y: [0, -5, 0],
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity, delay: 0.5 }}
|
||||
>
|
||||
✨
|
||||
</motion.span>
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
className="text-xl md:text-2xl text-gray-700 leading-relaxed max-w-3xl mx-auto"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
Our AI creates <span className="font-semibold text-warm-coral">personalized itineraries</span> with
|
||||
perfectly timed activities, optimized routes, and <span className="font-semibold text-warm-coral">curated experiences</span> tailored
|
||||
just for you.
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* Card Stack Display */}
|
||||
<div className="max-w-6xl w-full">
|
||||
<div className="relative flex justify-center items-center min-h-[600px] lg:min-h-[650px] perspective-1000">
|
||||
{/* Card Stack Container */}
|
||||
<div className="relative w-full max-w-md lg:max-w-lg" style={{ transformStyle: 'preserve-3d' }}>
|
||||
{/* Visible Card Stack - Third Card */}
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-white rounded-3xl shadow-lg overflow-hidden"
|
||||
style={{
|
||||
zIndex: 1,
|
||||
transformStyle: 'preserve-3d'
|
||||
}}
|
||||
animate={{
|
||||
scale: 0.88,
|
||||
y: 24,
|
||||
opacity: 0.4,
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="relative h-64 lg:h-80 overflow-hidden">
|
||||
<ImageWithFallback
|
||||
src={thirdCard.image}
|
||||
alt={`${thirdCard.city}, ${thirdCard.country}`}
|
||||
className="w-full h-full object-cover opacity-60"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
|
||||
<div className="absolute bottom-6 left-6 right-6">
|
||||
<h3 className="text-2xl lg:text-3xl font-bold text-white opacity-70">
|
||||
{thirdCard.city}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Visible Card Stack - Second Card */}
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-white rounded-3xl shadow-xl overflow-hidden"
|
||||
style={{
|
||||
zIndex: 2,
|
||||
transformStyle: 'preserve-3d'
|
||||
}}
|
||||
animate={{
|
||||
scale: 0.94,
|
||||
y: 12,
|
||||
opacity: 0.7,
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="relative h-64 lg:h-80 overflow-hidden">
|
||||
<ImageWithFallback
|
||||
src={nextCard.image}
|
||||
alt={`${nextCard.city}, ${nextCard.country}`}
|
||||
className="w-full h-full object-cover opacity-80"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
|
||||
<div className="absolute bottom-6 left-6 right-6">
|
||||
<h3 className="text-2xl lg:text-3xl font-bold text-white opacity-80">
|
||||
{nextCard.city}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Animated Front Card */}
|
||||
<AnimatePresence mode="popLayout">
|
||||
<motion.div
|
||||
key={currentCard.id}
|
||||
className="relative bg-white rounded-3xl shadow-2xl overflow-hidden"
|
||||
style={{
|
||||
zIndex: 3,
|
||||
transformStyle: 'preserve-3d'
|
||||
}}
|
||||
initial={{
|
||||
scale: 0.94,
|
||||
y: 12,
|
||||
opacity: 0.7,
|
||||
}}
|
||||
animate={{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
exit={{
|
||||
scale: 1.05,
|
||||
opacity: 0,
|
||||
x: -100,
|
||||
rotateZ: -5,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
ease: [0.34, 1.56, 0.64, 1], // Bounce easing
|
||||
}}
|
||||
>
|
||||
{/* Card Image */}
|
||||
<div className="relative h-64 lg:h-80 overflow-hidden">
|
||||
<ImageWithFallback
|
||||
src={currentCard.image}
|
||||
alt={`${currentCard.city}, ${currentCard.country}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
|
||||
|
||||
{/* City Info Overlay */}
|
||||
<motion.div
|
||||
className="absolute bottom-6 left-6 right-6"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2, duration: 0.5, ease: [0.34, 1.56, 0.64, 1] }}
|
||||
>
|
||||
<h3 className="text-3xl lg:text-4xl font-bold text-white mb-2">
|
||||
{currentCard.city}
|
||||
</h3>
|
||||
<p className="text-white/90 text-lg flex items-center gap-2">
|
||||
<MapPin className="w-5 h-5" />
|
||||
{currentCard.country}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Duration Badge */}
|
||||
<motion.div
|
||||
className="absolute top-6 right-6 bg-gradient-to-r from-warm-coral to-orange-500 px-4 py-2 rounded-full shadow-xl"
|
||||
initial={{ scale: 0, opacity: 0, rotate: -180 }}
|
||||
animate={{ scale: 1, opacity: 1, rotate: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.6, type: "spring", stiffness: 200, damping: 12 }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 text-white" />
|
||||
<span className="font-bold text-white">{currentCard.days} Days</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Top Left Journey Icon */}
|
||||
<motion.div
|
||||
className="absolute top-6 left-6 bg-white/95 backdrop-blur-sm p-3 rounded-full shadow-lg"
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.5, type: "spring" }}
|
||||
>
|
||||
<Plane className="w-5 h-5 text-warm-coral" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Card Content */}
|
||||
<motion.div
|
||||
className="p-4 lg:p-5"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3, duration: 0.5 }}
|
||||
>
|
||||
{/* Highlights - Compact */}
|
||||
<motion.div
|
||||
className="flex flex-wrap gap-1.5 mb-4"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.4 }}
|
||||
>
|
||||
{currentCard.highlights.map((highlight, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-3 py-1 bg-gradient-to-r from-warm-coral/15 to-orange-100 text-warm-coral border border-warm-coral/20 rounded-full text-xs font-semibold shadow-sm transition-transform hover:scale-105"
|
||||
>
|
||||
{highlight}
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Day 1 Header */}
|
||||
<motion.div
|
||||
className="flex items-center gap-2 mb-2.5"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.35, duration: 0.4 }}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 px-3 py-1 bg-gradient-to-r from-warm-coral to-orange-500 rounded-full shadow-md">
|
||||
<Clock className="w-3 h-3 text-white" />
|
||||
<span className="text-xs font-bold text-white">Day 1</span>
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-warm-coral/30 to-transparent" />
|
||||
</motion.div>
|
||||
|
||||
{/* Day 1 Activities - Compact */}
|
||||
<motion.div
|
||||
className="space-y-2 mb-3"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4, duration: 0.4 }}
|
||||
>
|
||||
{currentCard.activities.slice(0, 2).map((activity, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="relative flex items-center gap-2.5 p-2.5 bg-gradient-to-r from-white to-orange-50/30 rounded-xl border border-warm-coral/10 hover:border-warm-coral/30 hover:shadow-md transition-all group hover:translate-x-0.5"
|
||||
>
|
||||
{/* Route Line Connector */}
|
||||
{idx < 1 && (
|
||||
<div className="absolute left-4 top-full h-2 w-0.5 bg-gradient-to-b from-warm-coral/50 to-transparent" />
|
||||
)}
|
||||
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-7 h-7 bg-gradient-to-br from-warm-coral to-orange-500 rounded-full flex items-center justify-center shadow-md">
|
||||
<span className="text-white font-bold text-xs">{idx + 1}</span>
|
||||
</div>
|
||||
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-green-400 rounded-full border border-white" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-semibold text-gray-900 group-hover:text-warm-coral transition-colors leading-tight mb-0.5">{activity.name}</p>
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-500">
|
||||
<Clock className="w-2.5 h-2.5" />
|
||||
<span>{activity.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Day 2 Header */}
|
||||
<motion.div
|
||||
className="flex items-center gap-2 mb-2.5"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.45, duration: 0.4 }}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 px-3 py-1 bg-gradient-to-r from-orange-500 to-rose-500 rounded-full shadow-md">
|
||||
<Clock className="w-3 h-3 text-white" />
|
||||
<span className="text-xs font-bold text-white">Day 2</span>
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-orange-500/30 to-transparent" />
|
||||
</motion.div>
|
||||
|
||||
{/* Day 2 Activities - Compact */}
|
||||
<motion.div
|
||||
className="space-y-2"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5, duration: 0.4 }}
|
||||
>
|
||||
{currentCard.activities.slice(2, 4).map((activity, idx) => (
|
||||
<div
|
||||
key={idx + 2}
|
||||
className="relative flex items-center gap-2.5 p-2.5 bg-gradient-to-r from-white to-rose-50/30 rounded-xl border border-orange-500/10 hover:border-orange-500/30 hover:shadow-md transition-all group hover:translate-x-0.5"
|
||||
>
|
||||
{/* Route Line Connector */}
|
||||
{idx < 1 && (
|
||||
<div className="absolute left-4 top-full h-2 w-0.5 bg-gradient-to-b from-orange-500/50 to-transparent" />
|
||||
)}
|
||||
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-7 h-7 bg-gradient-to-br from-orange-500 to-rose-500 rounded-full flex items-center justify-center shadow-md">
|
||||
<span className="text-white font-bold text-xs">{idx + 1}</span>
|
||||
</div>
|
||||
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-green-400 rounded-full border border-white" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-semibold text-gray-900 group-hover:text-orange-500 transition-colors leading-tight mb-0.5">{activity.name}</p>
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-500">
|
||||
<Clock className="w-2.5 h-2.5" />
|
||||
<span>{activity.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Optimized Floating Sparkles - Reduced from 8 to 3 */}
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="absolute pointer-events-none will-change-transform"
|
||||
style={{
|
||||
top: `${20 + i * 30}%`,
|
||||
left: `${10 + i * 40}%`,
|
||||
}}
|
||||
animate={{
|
||||
y: [0, -20, 0],
|
||||
opacity: [0, 0.5, 0],
|
||||
scale: [0, 1, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
delay: i * 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
<Sparkles className="w-5 h-5 text-warm-coral" />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Indicators with City Names */}
|
||||
<motion.div
|
||||
className="flex flex-wrap justify-center gap-3 mt-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 1 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{itineraryCards.map((card, idx) => (
|
||||
<motion.button
|
||||
key={card.id}
|
||||
onClick={() => {
|
||||
setIsAnimating(true);
|
||||
setTimeout(() => {
|
||||
setCurrentCardIndex(idx);
|
||||
setIsAnimating(false);
|
||||
}, 400);
|
||||
}}
|
||||
className={`group relative transition-all duration-300 px-4 py-2 rounded-full font-medium ${
|
||||
idx === currentCardIndex
|
||||
? 'bg-gradient-to-r from-warm-coral to-orange-500 text-white shadow-lg scale-110'
|
||||
: 'bg-white/80 backdrop-blur-sm text-gray-600 hover:text-warm-coral hover:bg-white border border-gray-200 hover:border-warm-coral/30 hover:scale-105'
|
||||
}`}
|
||||
whileHover={{ y: -2 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{card.city}
|
||||
{idx === currentCardIndex && (
|
||||
<motion.div
|
||||
className="absolute -bottom-1 left-1/2 w-1.5 h-1.5 bg-white rounded-full"
|
||||
layoutId="activeIndicator"
|
||||
initial={false}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||
style={{ x: '-50%' }}
|
||||
/>
|
||||
)}
|
||||
</motion.button>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* CTA Button - Optimized */}
|
||||
<motion.div
|
||||
className="flex flex-col items-center gap-6 mt-16"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<Button
|
||||
withShine={true}
|
||||
className="py-7 px-16 rounded-full text-xl font-bold bg-gradient-to-r from-warm-coral via-orange-500 to-rose-500 hover:from-warm-coral/90 hover:via-orange-500/90 hover:to-rose-500/90 shadow-2xl hover:shadow-warm-coral/50 transition-all hover:scale-105 hover:-translate-y-1"
|
||||
>
|
||||
<span className="flex items-center gap-3">
|
||||
<Wand2 className="w-6 h-6" />
|
||||
Create My Perfect Itinerary
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<p className="text-gray-600 text-sm flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-warm-coral" />
|
||||
<span>Free to use • No credit card required</span>
|
||||
<Sparkles className="w-4 h-4 text-warm-coral" />
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
642
src/components/LandingMobileAppSection.tsx
Normal file
642
src/components/LandingMobileAppSection.tsx
Normal file
@@ -0,0 +1,642 @@
|
||||
import { ArrowRight, Smartphone, MapPin, Star, Clock, Users, Heart, Share2, Filter, Search } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
import imgFrame1597884939 from "figma:asset/5da1b0444c0d21bc7ee776c49e36e2a8ea4d3e12.png";
|
||||
|
||||
export function LandingMobileAppSection() {
|
||||
// Generate a realistic QR code pattern
|
||||
const generateQRPattern = () => {
|
||||
const size = 21; // Standard QR code size
|
||||
const pattern = [];
|
||||
|
||||
for (let i = 0; i < size * size; i++) {
|
||||
// Create corner detection patterns
|
||||
const row = Math.floor(i / size);
|
||||
const col = i % size;
|
||||
|
||||
// Corner squares (7x7)
|
||||
const isCornerSquare =
|
||||
(row < 7 && col < 7) || // Top-left
|
||||
(row < 7 && col >= 14) || // Top-right
|
||||
(row >= 14 && col < 7); // Bottom-left
|
||||
|
||||
// Finder patterns within corner squares
|
||||
const isFinderPattern = isCornerSquare && (
|
||||
(row === 0 || row === 6 || col === 0 || col === 6) ||
|
||||
(row >= 2 && row <= 4 && col >= 2 && col <= 4)
|
||||
);
|
||||
|
||||
// Timing patterns
|
||||
const isTimingPattern = (row === 6 && col >= 8 && col <= 12) || (col === 6 && row >= 8 && row <= 12);
|
||||
|
||||
// Random data pattern for other areas
|
||||
const isDataPattern = !isCornerSquare && !isTimingPattern && Math.random() > 0.45;
|
||||
|
||||
pattern.push(isFinderPattern || isTimingPattern || isDataPattern);
|
||||
}
|
||||
|
||||
return pattern;
|
||||
};
|
||||
|
||||
const qrPattern = generateQRPattern();
|
||||
|
||||
return (
|
||||
<section className="py-20 lg:py-32 bg-muted/30 relative overflow-hidden">
|
||||
{/* Subtle Background Elements */}
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute top-1/3 left-1/6 w-64 h-64 bg-warm-coral/3 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-1/2 right-1/6 w-48 h-48 bg-warm-coral/3 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
{/* Figma Layout Implementation */}
|
||||
<motion.div
|
||||
className="flex flex-col gap-16 lg:gap-20"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
>
|
||||
{/* Header Section - Following Figma Layout */}
|
||||
<div className="flex flex-col lg:flex-row items-start justify-between gap-12 lg:gap-16">
|
||||
{/* Left Side - Main Heading */}
|
||||
<motion.div
|
||||
className="flex-1 max-w-2xl"
|
||||
initial={{ opacity: 0, x: -40 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.7, ease: "easeOut" }}
|
||||
>
|
||||
<h1 className="text-4xl lg:text-5xl xl:text-6xl leading-tight text-foreground">
|
||||
<span className="font-normal">Your</span>{' '}
|
||||
<span className="text-warm-coral font-bold italic">
|
||||
Melbourne
|
||||
</span>
|
||||
<br />
|
||||
<span className="font-normal">City Card in Your</span>{' '}
|
||||
<span className="font-semibold">Pocket.</span>
|
||||
</h1>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Side - Description and Buttons */}
|
||||
<motion.div
|
||||
className="flex flex-col gap-8 w-full lg:w-[400px] xl:w-[450px]"
|
||||
initial={{ opacity: 0, x: 40 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.7, ease: "easeOut", delay: 0.1 }}
|
||||
>
|
||||
{/* Description Text */}
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
Download our mobile app and unlock instant access to premium city experiences across Australia.
|
||||
</p>
|
||||
|
||||
{/* Download Buttons - Following Figma Layout */}
|
||||
<div className="flex flex-col sm:flex-row gap-6">
|
||||
{/* Android Download Button */}
|
||||
<motion.button
|
||||
className="interactive-button relative flex items-center gap-3 bg-foreground text-background px-6 py-4 rounded-xl font-medium text-base hover:bg-foreground/90 transition-all duration-300 shadow-lg hover:shadow-xl flex-1 overflow-hidden group"
|
||||
whileHover={{ scale: 1.02, y: -2 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{/* Continuous Shine Effect */}
|
||||
<div className="absolute inset-0 -top-px opacity-100">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/25 to-transparent transform -skew-x-12 animate-shine"></div>
|
||||
</div>
|
||||
|
||||
{/* Google Play Logo */}
|
||||
<svg className="w-6 h-6 relative z-10" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3,20.5V3.5C3,2.91 3.34,2.39 3.84,2.15L13.69,12L3.84,21.85C3.34,21.6 3,21.09 3,20.5M16.81,15.12L6.05,21.34L14.54,12.85L16.81,15.12M20.16,10.81C20.5,11.08 20.75,11.5 20.75,12C20.75,12.5 20.53,12.9 20.18,13.18L17.89,14.5L15.39,12L17.89,9.5L20.16,10.81M6.05,2.66L16.81,8.88L14.54,11.15L6.05,2.66Z"/>
|
||||
</svg>
|
||||
<div className="text-left relative z-10">
|
||||
<div className="text-xs opacity-80">Get it on</div>
|
||||
<div className="font-semibold">Google Play</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
|
||||
{/* iOS Download Button */}
|
||||
<motion.button
|
||||
className="interactive-button relative flex items-center gap-3 bg-foreground text-background px-6 py-4 rounded-xl font-medium text-base hover:bg-foreground/90 transition-all duration-300 shadow-lg hover:shadow-xl flex-1 overflow-hidden group"
|
||||
whileHover={{ scale: 1.02, y: -2 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{/* Continuous Shine Effect */}
|
||||
<div className="absolute inset-0 -top-px opacity-100">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/25 to-transparent transform -skew-x-12 animate-shine"></div>
|
||||
</div>
|
||||
|
||||
{/* Apple Logo */}
|
||||
<svg className="w-6 h-6 relative z-10" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
|
||||
</svg>
|
||||
<div className="text-left relative z-10">
|
||||
<div className="text-xs opacity-80">Download on the</div>
|
||||
<div className="font-semibold">App Store</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Mockups Section - Ultra-Realistic and Larger */}
|
||||
<motion.div
|
||||
className="relative w-full h-[400px] lg:h-[500px] rounded-3xl overflow-hidden shadow-2xl"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, ease: "easeOut", delay: 0.2 }}
|
||||
style={{
|
||||
background: `url(${imgFrame1597884939}) center/cover`
|
||||
}}
|
||||
>
|
||||
{/* Mobile Mockup 1 - Enhanced Main App Screen */}
|
||||
<div className="absolute top-[-6rem] lg:top-[-7rem] right-[calc(50%-100px-8.25rem)] lg:right-[calc(50%-100px-10.25rem)]">
|
||||
<motion.div
|
||||
className="w-60 lg:w-64 xl:w-72 h-[420px] lg:h-[480px] xl:h-[540px] bg-white rounded-[2.5rem] shadow-2xl border-4 lg:border-8 border-gray-900 relative overflow-hidden"
|
||||
initial={{ opacity: 0, y: -30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
whileHover={{ scale: 1.02, rotateY: 2 }}
|
||||
>
|
||||
{/* iPhone Status Bar */}
|
||||
<div className="flex justify-between items-center px-6 py-3 bg-white">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-semibold text-black">9:41</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Signal bars */}
|
||||
<div className="flex gap-0.5">
|
||||
{[1, 2, 3, 4].map((bar) => (
|
||||
<div key={bar} className={`w-1 bg-black rounded-full ${bar === 1 ? 'h-1' : bar === 2 ? 'h-2' : bar === 3 ? 'h-3' : 'h-4'}`}></div>
|
||||
))}
|
||||
</div>
|
||||
{/* WiFi */}
|
||||
<svg className="w-4 h-4 ml-1" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/>
|
||||
</svg>
|
||||
{/* Battery */}
|
||||
<div className="w-6 h-3 border border-black rounded-sm ml-1">
|
||||
<div className="w-4 h-1.5 bg-green-500 rounded-sm m-0.5"></div>
|
||||
</div>
|
||||
<div className="w-0.5 h-2 bg-black rounded-r-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* App Header */}
|
||||
<div className="px-5 py-4 bg-warm-coral">
|
||||
<div className="flex items-center justify-between text-white mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-white/20 rounded-xl flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold">CityCards</h3>
|
||||
<div className="text-xs text-white/70">Premium Active</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Search className="w-5 h-5 text-white/80" />
|
||||
<div className="absolute -top-1 -right-1 w-2 h-2 bg-orange-400 rounded-full"></div>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<div className="w-4 h-4 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div className="flex items-center justify-between text-white/90">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">Sydney, NSW</span>
|
||||
<div className="w-1 h-1 bg-white/60 rounded-full"></div>
|
||||
<span className="text-xs">22°C ☀️</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs bg-white/20 px-3 py-1.5 rounded-full">
|
||||
<span className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
|
||||
<span>3 active</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* App Content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* Today's Deal Banner */}
|
||||
<div className="mx-5 my-4 bg-gradient-to-r from-purple-100 via-blue-100 to-indigo-100 rounded-xl p-4 border border-purple-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold text-purple-700 bg-purple-200 px-2 py-1 rounded-full">TODAY ONLY</span>
|
||||
<div className="flex items-center gap-1 text-xs text-purple-600">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>8h left</span>
|
||||
</div>
|
||||
</div>
|
||||
<Heart className="w-4 h-4 text-red-400" />
|
||||
</div>
|
||||
<h4 className="font-bold text-gray-900 text-sm mb-1">Sydney Explorer Pass</h4>
|
||||
<p className="text-xs text-gray-700 mb-3">Visit 3+ attractions & get 40% off</p>
|
||||
<div className="flex gap-2">
|
||||
<button className="bg-purple-600 text-white px-3 py-1.5 rounded-lg text-xs font-semibold">
|
||||
Activate
|
||||
</button>
|
||||
<button className="border border-purple-300 text-purple-700 px-3 py-1.5 rounded-lg text-xs font-medium">
|
||||
Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Bar */}
|
||||
<div className="mx-5 mb-4">
|
||||
<div className="bg-gray-50 rounded-xl p-3 border border-gray-100">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-5 h-5 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-green-700">3</span>
|
||||
</div>
|
||||
<span className="text-gray-700 text-xs">Passes used today</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-bold text-green-600 text-sm">$127</div>
|
||||
<div className="text-xs text-gray-500">saved</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Attraction Cards */}
|
||||
<div className="mx-5 space-y-3">
|
||||
{/* Opera House */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-3 shadow-sm relative">
|
||||
<div className="absolute top-2 right-2 bg-yellow-100 text-yellow-600 text-xs font-bold px-2 py-1 rounded-full">
|
||||
PREMIUM
|
||||
</div>
|
||||
<div className="flex gap-3 mb-2">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.94-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900 text-sm">Sydney Opera House</h4>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-600 mt-0.5">
|
||||
<Star className="w-3 h-3 text-yellow-400 fill-current" />
|
||||
<span className="font-medium">4.8</span>
|
||||
<span className="text-gray-400">(2.4k)</span>
|
||||
<span className="mx-1">•</span>
|
||||
<MapPin className="w-3 h-3 text-blue-500" />
|
||||
<span>2.3km</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Heart className="w-3 h-3 text-gray-400" />
|
||||
<Share2 className="w-3 h-3 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-2 text-xs">
|
||||
<div className="bg-orange-100 text-orange-700 px-2 py-0.5 rounded-full font-medium">
|
||||
Architecture
|
||||
</div>
|
||||
<div className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-medium">
|
||||
Cultural
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-base font-bold text-gray-900">$45</span>
|
||||
<span className="text-xs text-gray-500 line-through">$75</span>
|
||||
</div>
|
||||
<div className="text-xs text-green-600 font-medium">Save $30</div>
|
||||
</div>
|
||||
<div className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full border border-green-200">
|
||||
40% OFF
|
||||
</div>
|
||||
</div>
|
||||
<button className="bg-warm-coral text-white px-3 py-1.5 rounded-lg font-semibold text-xs">
|
||||
Use Pass
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Harbour Bridge */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-3 shadow-sm relative">
|
||||
<div className="absolute top-2 right-2 bg-blue-100 text-blue-600 text-xs font-bold px-2 py-1 rounded-full">
|
||||
POPULAR
|
||||
</div>
|
||||
<div className="flex gap-3 mb-2">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-emerald-500 to-green-600 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900 text-sm">Harbour Bridge Climb</h4>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-600 mt-0.5">
|
||||
<Star className="w-3 h-3 text-yellow-400 fill-current" />
|
||||
<span className="font-medium">4.9</span>
|
||||
<span className="text-gray-400">(1.8k)</span>
|
||||
<span className="mx-1">•</span>
|
||||
<MapPin className="w-3 h-3 text-blue-500" />
|
||||
<span>1.8km</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Heart className="w-3 h-3 text-red-400 fill-current" />
|
||||
<Share2 className="w-3 h-3 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-2 text-xs">
|
||||
<div className="bg-green-100 text-green-700 px-2 py-0.5 rounded-full font-medium">
|
||||
Adventure
|
||||
</div>
|
||||
<div className="bg-red-100 text-red-700 px-2 py-0.5 rounded-full font-medium">
|
||||
Iconic
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-base font-bold text-gray-900">$159</span>
|
||||
<span className="text-xs text-gray-500 line-through">$289</span>
|
||||
</div>
|
||||
<div className="text-xs text-green-600 font-medium">Save $130</div>
|
||||
</div>
|
||||
<div className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full border border-blue-200">
|
||||
45% OFF
|
||||
</div>
|
||||
</div>
|
||||
<button className="bg-primary text-white px-3 py-1.5 rounded-lg font-semibold text-xs">
|
||||
Use Pass
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botanic Gardens */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-3 shadow-sm relative">
|
||||
<div className="absolute top-2 right-2 bg-emerald-100 text-emerald-600 text-xs font-bold px-2 py-1 rounded-full">
|
||||
FREE
|
||||
</div>
|
||||
<div className="flex gap-3 mb-2">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-green-400 to-emerald-500 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M17,8C8,10 5.9,16.17 3.82,21.34L5.71,22L6.66,19.7C7.14,19.87 7.64,20 8,20C19,20 22,3 22,3C21,5 14,5.25 9,6.25C4,7.25 2,11.5 2,13.5C2,15.5 3.75,17.25 3.75,17.25C7,8 17,8 17,8Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900 text-sm">Royal Botanic Gardens</h4>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-600 mt-0.5">
|
||||
<Star className="w-3 h-3 text-yellow-400 fill-current" />
|
||||
<span className="font-medium">4.7</span>
|
||||
<span className="text-gray-400">(956)</span>
|
||||
<span className="mx-1">•</span>
|
||||
<MapPin className="w-3 h-3 text-blue-500" />
|
||||
<span>1.2km</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Heart className="w-3 h-3 text-gray-400" />
|
||||
<Share2 className="w-3 h-3 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-2 text-xs">
|
||||
<div className="bg-green-100 text-green-700 px-2 py-0.5 rounded-full font-medium">
|
||||
Nature
|
||||
</div>
|
||||
<div className="bg-purple-100 text-purple-700 px-2 py-0.5 rounded-full font-medium">
|
||||
Family
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<span className="text-base font-bold text-green-600">FREE</span>
|
||||
<div className="text-xs text-green-600 font-medium">Included</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="bg-green-600 text-white px-3 py-1.5 rounded-lg font-semibold text-xs">
|
||||
Visit Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<div className="border-t border-gray-100 bg-white/95 backdrop-blur-sm">
|
||||
<div className="flex items-center justify-around py-3">
|
||||
{[
|
||||
{
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
|
||||
</svg>
|
||||
),
|
||||
label: "Home",
|
||||
active: true,
|
||||
badge: null
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
||||
</svg>
|
||||
),
|
||||
label: "Map",
|
||||
active: false,
|
||||
badge: "3"
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M22 10v6c0 1.11-.89 2-2 2H4c-1.11 0-2-.89-2-2v-8c0-1.11.89-2 2-2h14l2-2v6zm-9 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>
|
||||
</svg>
|
||||
),
|
||||
label: "Passes",
|
||||
active: false,
|
||||
badge: null
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
|
||||
</svg>
|
||||
),
|
||||
label: "Activity",
|
||||
active: false,
|
||||
badge: null
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||
</svg>
|
||||
),
|
||||
label: "Profile",
|
||||
active: false,
|
||||
badge: null
|
||||
}
|
||||
].map((item, index) => (
|
||||
<div key={index} className={`flex flex-col items-center justify-center gap-1 py-1 relative ${item.active ? 'text-primary' : 'text-gray-500'}`}>
|
||||
<div className="relative">
|
||||
{item.icon}
|
||||
{item.badge && (
|
||||
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full min-w-[16px] h-4 flex items-center justify-center px-1">
|
||||
{item.badge}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className={`text-xs ${item.active ? 'font-semibold' : 'font-medium'}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
{item.active && (
|
||||
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-4 h-0.5 bg-primary rounded-full"></div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Mockup 2 - Map View */}
|
||||
<div className="absolute bottom-[-6rem] lg:bottom-[-7rem] left-[calc(50%-100px-8.25rem)] lg:left-[calc(50%-100px-10.25rem)]">
|
||||
<motion.div
|
||||
className="w-56 lg:w-60 xl:w-64 h-[380px] lg:h-[440px] xl:h-[480px] bg-white rounded-[2.5rem] shadow-2xl border-4 lg:border-8 border-gray-900 relative overflow-hidden"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: 0.6 }}
|
||||
whileHover={{ scale: 1.02, rotateY: -2 }}
|
||||
>
|
||||
{/* Status Bar */}
|
||||
<div className="flex justify-between items-center px-6 py-3 bg-white">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-semibold text-black">9:41</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex gap-0.5">
|
||||
{[1, 2, 3, 4].map((bar) => (
|
||||
<div key={bar} className={`w-1 bg-black rounded-full ${bar === 1 ? 'h-1' : bar === 2 ? 'h-2' : bar === 3 ? 'h-3' : 'h-4'}`}></div>
|
||||
))}
|
||||
</div>
|
||||
<svg className="w-4 h-4 ml-1" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/>
|
||||
</svg>
|
||||
<div className="w-6 h-3 border border-black rounded-sm ml-1">
|
||||
<div className="w-4 h-1.5 bg-green-500 rounded-sm m-0.5"></div>
|
||||
</div>
|
||||
<div className="w-0.5 h-2 bg-black rounded-r-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Map Header */}
|
||||
<div className="px-5 py-3 bg-white border-b border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="w-8 h-8 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
<h3 className="font-bold text-gray-900">Explore Sydney</h3>
|
||||
<div className="text-xs text-gray-500">3 nearby attractions</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||
<Filter className="w-4 h-4 text-primary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Map Content */}
|
||||
<div className="flex-1 bg-gradient-to-br from-green-100 to-blue-100 relative overflow-hidden">
|
||||
{/* Simplified Map Pattern */}
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<div className="w-full h-full bg-gradient-to-br from-blue-200 via-green-200 to-yellow-200"></div>
|
||||
{/* Street lines */}
|
||||
<div className="absolute top-1/4 left-0 right-0 h-px bg-gray-400 opacity-30"></div>
|
||||
<div className="absolute top-1/2 left-0 right-0 h-px bg-gray-400 opacity-40"></div>
|
||||
<div className="absolute top-3/4 left-0 right-0 h-px bg-gray-400 opacity-30"></div>
|
||||
<div className="absolute left-1/4 top-0 bottom-0 w-px bg-gray-400 opacity-30"></div>
|
||||
<div className="absolute left-1/2 top-0 bottom-0 w-px bg-gray-400 opacity-40"></div>
|
||||
<div className="absolute left-3/4 top-0 bottom-0 w-px bg-gray-400 opacity-30"></div>
|
||||
</div>
|
||||
|
||||
{/* Location Pins */}
|
||||
<div className="absolute top-8 left-8 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
||||
<div className="absolute top-16 right-12 w-3 h-3 bg-blue-500 rounded-full animate-pulse"></div>
|
||||
<div className="absolute bottom-20 left-12 w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<div className="absolute bottom-12 right-8 w-3 h-3 bg-purple-500 rounded-full animate-pulse"></div>
|
||||
|
||||
{/* Current Location */}
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="relative">
|
||||
<div className="w-6 h-6 bg-blue-600 rounded-full border-4 border-white shadow-lg animate-pulse"></div>
|
||||
<div className="absolute inset-0 w-6 h-6 bg-blue-600 rounded-full animate-ping opacity-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Route Line */}
|
||||
<svg className="absolute inset-0 w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<path
|
||||
d="M20,30 Q40,20 60,40 T80,70"
|
||||
stroke="#6366f1"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
strokeDasharray="5,5"
|
||||
className="animate-pulse"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Bottom Card */}
|
||||
<div className="absolute bottom-4 left-4 right-4 bg-white rounded-xl p-3 shadow-lg border border-gray-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.94-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-bold text-gray-900 text-sm">Sydney Opera House</h4>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-600">
|
||||
<MapPin className="w-3 h-3 text-blue-500" />
|
||||
<span>2.3km away • 5 min walk</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="bg-primary text-white px-3 py-1.5 rounded-lg font-semibold text-xs">
|
||||
Go
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
181
src/components/LandingNewsletterSection.tsx
Normal file
181
src/components/LandingNewsletterSection.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Mail } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
|
||||
export function LandingNewsletterSection() {
|
||||
return (
|
||||
<section className="relative min-h-screen overflow-hidden bg-gradient-to-br from-primary/5 via-white to-secondary/5">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 opacity-30">
|
||||
<div className="absolute top-20 left-20 w-32 h-32 bg-primary/10 rounded-full blur-3xl animate-float"></div>
|
||||
<div className="absolute bottom-32 right-16 w-40 h-40 bg-secondary/10 rounded-full blur-3xl animate-float animate-delay-1000"></div>
|
||||
<div className="absolute top-1/2 left-1/4 w-24 h-24 bg-primary/5 rounded-full blur-2xl"></div>
|
||||
<div className="absolute bottom-1/4 right-1/3 w-36 h-36 bg-secondary/5 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
{/* Subtle Grid Pattern */}
|
||||
<div className="absolute inset-0 opacity-5">
|
||||
<div className="w-full h-full" style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '50px 50px'
|
||||
}}></div>
|
||||
</div>
|
||||
|
||||
{/* Floating Email Icons */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<motion.div
|
||||
className="absolute top-1/4 left-10 text-primary/20"
|
||||
animate={{
|
||||
y: [0, -20, 0],
|
||||
rotate: [0, 5, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
>
|
||||
<Mail className="w-8 h-8" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute top-1/3 right-16 text-secondary/20"
|
||||
animate={{
|
||||
y: [0, 15, 0],
|
||||
rotate: [0, -5, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 8,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 2
|
||||
}}
|
||||
>
|
||||
<Mail className="w-6 h-6" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="absolute bottom-1/3 left-1/4 text-primary/15"
|
||||
animate={{
|
||||
y: [0, -10, 0],
|
||||
rotate: [0, 3, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 7,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 4
|
||||
}}
|
||||
>
|
||||
<Mail className="w-5 h-5" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="relative z-10 py-20 lg:py-28 text-center space-y-8 px-4">
|
||||
{/* Main Heading with Typography Guidelines */}
|
||||
<motion.div
|
||||
className="space-y-4"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
>
|
||||
<h2 className="font-poppins text-5xl md:text-6xl lg:text-7xl leading-tight">
|
||||
<div className="font-light">Get</div>
|
||||
<div>
|
||||
<span className="font-bold text-primary italic">
|
||||
travel tips
|
||||
</span>
|
||||
<span className="font-light"> &</span>
|
||||
</div>
|
||||
<div className="font-semibold">exclusive offers.</div>
|
||||
</h2>
|
||||
<motion.p
|
||||
className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-2xl mx-auto"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
We recommend you to subscribe, drop your email below to get daily update about us
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
{/* Newsletter Subscription Interface */}
|
||||
<motion.div
|
||||
className="max-w-2xl mx-auto space-y-6"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
{/* Email Subscription Bar */}
|
||||
<div className="relative">
|
||||
<div className="flex items-center space-x-3 bg-white rounded-full p-2 shadow-xl border border-gray-100/50">
|
||||
<div className="flex-1 relative">
|
||||
<Mail className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter your email address"
|
||||
className="font-poppins pl-12 pr-4 h-14 text-base border-0 focus:ring-0 focus:border-0 bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Button className="font-poppins font-semibold bg-primary hover:bg-primary/90 py-6 px-12 rounded-full text-lg shadow-lg text-white">
|
||||
Subscribe Now
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trust Indicators */}
|
||||
<motion.div
|
||||
className="font-poppins flex flex-wrap justify-center items-center gap-4 text-sm font-normal text-gray-500"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.5 }}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>No spam, unsubscribe anytime</span>
|
||||
</div>
|
||||
<div className="hidden md:block">•</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span>Read our Privacy Policy</span>
|
||||
</div>
|
||||
<div className="hidden md:block">•</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-primary rounded-full"></div>
|
||||
<span>Join 10,000+ subscribers</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Additional Info */}
|
||||
<motion.p
|
||||
className="text-sm text-gray-500 mt-4"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: 0.8 }}
|
||||
>
|
||||
Get exclusive travel tips, destination guides, and special offers delivered to your inbox
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Decorative Elements */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-white/50 to-transparent pointer-events-none"></div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
432
src/components/LandingTrustSection.tsx
Normal file
432
src/components/LandingTrustSection.tsx
Normal file
@@ -0,0 +1,432 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { motion, AnimatePresence, useMotionValue, useTransform, PanInfo } from 'motion/react';
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Sarah Mitchell',
|
||||
role: 'Travel Blogger',
|
||||
company: 'Wanderlust Adventures',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b1ac?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
|
||||
quote: 'CityCards completely transformed our Australian city-hopping adventure. The curated attraction passes saved us 60% on costs and the skip-the-line access was invaluable. Every city felt like a personalized experience tailored just for us.',
|
||||
signature: 'Sarah M.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Michael Chen',
|
||||
role: 'Travel Photographer',
|
||||
company: 'Urban Lens Studio',
|
||||
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
|
||||
quote: 'As someone who captures cities worldwide, CityCards gave me access to unique perspectives and hidden gems across Australia. The mobile city guide made discovering photo spots seamless, from Sydney\'s harbor to Melbourne\'s laneways.',
|
||||
signature: 'Michael C.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Emma Rodriguez',
|
||||
role: 'Adventure Seeker',
|
||||
company: 'Solo Travel Co.',
|
||||
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
|
||||
quote: 'Solo traveling became effortless with CityCards. From instant bookings to local recommendations, I felt confident exploring Australian cities. The comprehensive city guides unlocked experiences I never knew existed.',
|
||||
signature: 'Emma R.'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'David Park',
|
||||
role: 'Family Traveler',
|
||||
company: 'Adventure Families',
|
||||
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
|
||||
quote: 'Planning family trips used to be overwhelming, but CityCards simplified everything. The kids loved the interactive city experiences and we saved hours with skip-the-line access at every single attraction we visited.',
|
||||
signature: 'David P.'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Lisa Thompson',
|
||||
role: 'Business Traveler',
|
||||
company: 'Global Solutions Inc.',
|
||||
avatar: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
|
||||
quote: 'Between business meetings, CityCards helped me maximize my limited free time in every city. Quick access to top attractions without the hassle of traditional booking made every business trip memorable and productive.',
|
||||
signature: 'Lisa T.'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'James Wilson',
|
||||
role: 'Cultural Explorer',
|
||||
company: 'Heritage Travels',
|
||||
avatar: 'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
|
||||
quote: 'CityCards opened doors to authentic cultural experiences I would have missed otherwise. The curated recommendations led to discoveries that became the absolute highlights of our entire cultural journey through Australia\'s diverse cities.',
|
||||
signature: 'James W.'
|
||||
}
|
||||
];
|
||||
|
||||
// Custom SVG quotation marks
|
||||
const QuoteStart = ({ className }: { className?: string }) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 8C10 9.3 9.3 10 8 10C6.7 10 6 9.3 6 8C6 6.7 6.7 6 8 6C9.3 6 10 6.7 10 8ZM18 8C18 9.3 17.3 10 16 10C14.7 10 14 9.3 14 8C14 6.7 14.7 6 16 6C17.3 6 18 6.7 18 8ZM8 12L6 18H10L8 12ZM16 12L14 18H18L16 12Z" fill="currentColor"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const QuoteEnd = ({ className }: { className?: string }) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 16C14 14.7 14.7 14 16 14C17.3 14 18 14.7 18 16C18 17.3 17.3 18 16 18C14.7 18 14 17.3 14 16ZM6 16C6 14.7 6.7 14 8 14C9.3 14 10 14.7 10 16C10 17.3 9.3 18 8 18C6.7 18 6 17.3 6 16ZM16 12L18 6H14L16 12ZM8 12L10 6H6L8 12Z" fill="currentColor"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export function LandingTrustSection() {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [hoveredCard, setHoveredCard] = useState<number | null>(null);
|
||||
const [dragConstraints, setDragConstraints] = useState({ left: 0, right: 0 });
|
||||
const [showNameOnProgress, setShowNameOnProgress] = useState(false);
|
||||
const carouselRef = useRef<HTMLDivElement>(null);
|
||||
const x = useMotionValue(0);
|
||||
|
||||
// Calculate how many cards to show based on screen size
|
||||
const [cardsPerView, setCardsPerView] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
const updateCardsPerView = () => {
|
||||
if (window.innerWidth >= 1200) {
|
||||
setCardsPerView(2);
|
||||
} else {
|
||||
setCardsPerView(1);
|
||||
}
|
||||
};
|
||||
|
||||
updateCardsPerView();
|
||||
window.addEventListener('resize', updateCardsPerView);
|
||||
return () => window.removeEventListener('resize', updateCardsPerView);
|
||||
}, []);
|
||||
|
||||
const totalSlides = Math.ceil(testimonials.length / cardsPerView);
|
||||
const maxIndex = totalSlides - 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (carouselRef.current) {
|
||||
const cardWidth = carouselRef.current.offsetWidth;
|
||||
const maxDrag = -(cardWidth * maxIndex);
|
||||
setDragConstraints({ left: maxDrag, right: 0 });
|
||||
}
|
||||
}, [maxIndex, cardsPerView]);
|
||||
|
||||
const handlePrevious = () => {
|
||||
setCurrentIndex(prev => Math.max(0, prev - 1));
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentIndex(prev => Math.min(maxIndex, prev + 1));
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: any, info: PanInfo) => {
|
||||
const offset = info.offset.x;
|
||||
const velocity = info.velocity.x;
|
||||
|
||||
if (Math.abs(offset) > 100 || Math.abs(velocity) > 500) {
|
||||
if (offset > 0 && currentIndex > 0) {
|
||||
setCurrentIndex(prev => prev - 1);
|
||||
} else if (offset < 0 && currentIndex < maxIndex) {
|
||||
setCurrentIndex(prev => prev + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const progress = totalSlides > 1 ? (currentIndex / maxIndex) * 100 : 0;
|
||||
const getCurrentTestimonialNames = () => {
|
||||
const startIndex = currentIndex * cardsPerView;
|
||||
const endIndex = Math.min(startIndex + cardsPerView, testimonials.length);
|
||||
return testimonials.slice(startIndex, endIndex).map(t => t.name).join(', ');
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="py-16 md:py-24 bg-background relative overflow-hidden">
|
||||
{/* Subtle background texture */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.02]"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23000' fill-opacity='0.03'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3z'/%3E%3C/g%3E%3C/svg%3E")`
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="container mx-auto px-4" style={{ overflow: 'visible' }}>
|
||||
{/* Header */}
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight mb-6 text-foreground">
|
||||
<span className="font-light">What Our</span>{' '}
|
||||
<span className="font-bold italic text-primary">
|
||||
Travelers
|
||||
</span>{' '}
|
||||
<span className="font-light">Say</span>
|
||||
</h2>
|
||||
|
||||
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-700 max-w-2xl mx-auto">
|
||||
Real stories from real travelers who've discovered amazing cities with CityCards
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Carousel Container */}
|
||||
<div className="relative max-w-7xl mx-auto" style={{ overflow: 'visible' }}>
|
||||
|
||||
|
||||
{/* Carousel */}
|
||||
<div className="mx-8 py-6">
|
||||
<motion.div
|
||||
ref={carouselRef}
|
||||
className="flex"
|
||||
animate={{ x: `${-currentIndex * 100}%` }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||
drag="x"
|
||||
dragConstraints={dragConstraints}
|
||||
onDragEnd={handleDragEnd}
|
||||
style={{ cursor: 'grab', overflow: 'visible' }}
|
||||
whileDrag={{ cursor: 'grabbing' }}
|
||||
>
|
||||
{testimonials.map((testimonial, index) => {
|
||||
const cardRotation = (index % 3 - 1) * 1.5 + (Math.random() - 0.5) * 1;
|
||||
const cardOffset = (index % 2) * 10;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={testimonial.id}
|
||||
className="flex-shrink-0 px-8 py-4"
|
||||
style={{
|
||||
width: cardsPerView === 2 ? '50%' : '100%',
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
y: -5,
|
||||
transition: { duration: 0.2 }
|
||||
}}
|
||||
onHoverStart={() => setHoveredCard(testimonial.id)}
|
||||
onHoverEnd={() => setHoveredCard(null)}
|
||||
>
|
||||
{/* Paper Card with enhanced realism */}
|
||||
<div
|
||||
className="relative bg-white rounded-lg p-8"
|
||||
style={{
|
||||
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
|
||||
transformOrigin: 'center center',
|
||||
minHeight: '480px',
|
||||
background: `
|
||||
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),
|
||||
linear-gradient(145deg, #ffffff 0%, #fefefe 25%, #fdfdfd 50%, #fcfcfc 75%, #fbfbfb 100%)
|
||||
`,
|
||||
boxShadow: `
|
||||
0 8px 32px rgba(0, 0, 0, 0.12),
|
||||
0 4px 16px rgba(0, 0, 0, 0.08),
|
||||
0 2px 8px rgba(0, 0, 0, 0.06),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.02)
|
||||
`,
|
||||
border: '1px solid rgba(0, 0, 0, 0.04)',
|
||||
filter: hoveredCard === testimonial.id ? 'brightness(1.02)' : 'brightness(1)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
{/* Enhanced paper texture */}
|
||||
<div
|
||||
className="absolute inset-0 rounded-lg opacity-40 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f5f0e8' fill-opacity='0.4'%3E%3Cpath d='M10 10h1v1h-1zM20 15h1v1h-1zM30 25h1v1h-1zM40 30h1v1h-1zM50 40h1v1h-1zM15 50h1v1h-1z'/%3E%3C/g%3E%3C/svg%3E"),
|
||||
radial-gradient(circle at 25% 75%, rgba(245, 240, 232, 0.3) 0%, transparent 40%),
|
||||
radial-gradient(circle at 75% 25%, rgba(250, 245, 235, 0.2) 0%, transparent 35%)
|
||||
`,
|
||||
mixBlendMode: 'multiply'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Paper creases and folds */}
|
||||
<div
|
||||
className="absolute inset-0 rounded-lg opacity-20 pointer-events-none"
|
||||
style={{
|
||||
background: `
|
||||
linear-gradient(135deg, transparent 40%, rgba(0,0,0,0.02) 45%, rgba(0,0,0,0.01) 55%, transparent 60%),
|
||||
linear-gradient(45deg, transparent 30%, rgba(0,0,0,0.015) 35%, transparent 40%)
|
||||
`
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Corner fold effect */}
|
||||
<div
|
||||
className="absolute top-0 right-0 w-12 h-12 opacity-15 pointer-events-none"
|
||||
style={{
|
||||
background: `
|
||||
linear-gradient(-45deg,
|
||||
transparent 40%,
|
||||
rgba(0,0,0,0.08) 45%,
|
||||
rgba(0,0,0,0.12) 50%,
|
||||
rgba(0,0,0,0.08) 55%,
|
||||
transparent 60%
|
||||
)
|
||||
`,
|
||||
borderTopRightRadius: '8px',
|
||||
clipPath: 'polygon(60% 0%, 100% 0%, 100% 60%)'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Paperclip decoration */}
|
||||
<div
|
||||
className="absolute -top-2 -right-2 w-8 h-12 opacity-60 pointer-events-none z-10"
|
||||
style={{
|
||||
background: `
|
||||
linear-gradient(145deg, #e0e0e0 0%, #d0d0d0 50%, #c8c8c8 100%)
|
||||
`,
|
||||
borderRadius: '2px 2px 4px 4px',
|
||||
boxShadow: `
|
||||
0 2px 4px rgba(0,0,0,0.1),
|
||||
inset 0 1px 0 rgba(255,255,255,0.5),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.1)
|
||||
`,
|
||||
transform: 'rotate(8deg)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-1 border border-gray-400 rounded-sm"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tape effect on left edge */}
|
||||
<div
|
||||
className="absolute -left-1 top-16 w-4 h-16 opacity-30 pointer-events-none"
|
||||
style={{
|
||||
background: `
|
||||
linear-gradient(90deg,
|
||||
rgba(255,255,220,0.8) 0%,
|
||||
rgba(255,255,220,0.6) 50%,
|
||||
rgba(255,255,220,0.4) 100%
|
||||
)
|
||||
`,
|
||||
borderRadius: '2px',
|
||||
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.1)',
|
||||
transform: 'rotate(-2deg)'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Enhanced quotation marks */}
|
||||
<div className="mb-6">
|
||||
<QuoteStart className="w-8 h-8 text-amber-700/30 mb-4" />
|
||||
<p
|
||||
className="font-poppins text-base leading-relaxed font-normal text-foreground relative z-10"
|
||||
>
|
||||
{testimonial.quote}
|
||||
</p>
|
||||
<div className="flex justify-end mt-2">
|
||||
<QuoteEnd className="w-6 h-6 text-amber-700/30" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Profile Section with sticker effect */}
|
||||
<div className="mb-8 relative z-10">
|
||||
<div className="font-poppins text-lg leading-snug font-semibold text-foreground">
|
||||
{testimonial.name}
|
||||
</div>
|
||||
<div className="font-poppins text-sm leading-relaxed font-normal text-gray-600">
|
||||
{testimonial.company}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enhanced signature with writing animation */}
|
||||
<div className="flex justify-end relative z-10">
|
||||
<motion.div
|
||||
className="text-right transform -rotate-1"
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
animate={{
|
||||
pathLength: 1,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
pathLength: { duration: 2, delay: index * 0.1 },
|
||||
opacity: { duration: 0.5, delay: index * 0.1 }
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
fontFamily: "'Dancing Script', 'Brush Script MT', cursive",
|
||||
fontSize: '32px',
|
||||
color: 'rgba(101, 84, 63, 0.8)',
|
||||
textShadow: `
|
||||
1px 1px 2px rgba(0, 0, 0, 0.1),
|
||||
0 0 4px rgba(101, 84, 63, 0.2)
|
||||
`,
|
||||
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.1))'
|
||||
}}
|
||||
>
|
||||
{testimonial.signature}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Subtle aging spots */}
|
||||
<div
|
||||
className="absolute w-3 h-3 rounded-full opacity-8 pointer-events-none"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(160, 120, 80, 0.15) 0%, transparent 70%)',
|
||||
top: '15%',
|
||||
right: '20%'
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute w-2 h-2 rounded-full opacity-8 pointer-events-none"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(140, 110, 70, 0.12) 0%, transparent 70%)',
|
||||
bottom: '25%',
|
||||
left: '15%'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Pin shadow effect */}
|
||||
{index % 3 === 0 && (
|
||||
<div
|
||||
className="absolute w-2 h-2 rounded-full opacity-20 pointer-events-none"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(0,0,0,0.3) 0%, transparent 70%)',
|
||||
top: '8px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
filter: 'blur(1px)'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Progress Bar with hover names */}
|
||||
<div className="mt-10 max-w-md mx-auto">
|
||||
|
||||
|
||||
{/* Enhanced slide indicators */}
|
||||
<div className="flex justify-center space-x-3">
|
||||
{Array.from({ length: totalSlides }).map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCurrentIndex(index)}
|
||||
className={`w-4 h-4 rounded-full transition-all duration-300 transform ${
|
||||
index === currentIndex
|
||||
? 'bg-warm-coral scale-110 shadow-lg'
|
||||
: 'bg-gray-300 hover:bg-gray-400 hover:scale-105'
|
||||
}`}
|
||||
style={{
|
||||
boxShadow: index === currentIndex
|
||||
? '0 4px 8px rgba(249, 95, 98, 0.3), 0 2px 4px rgba(249, 95, 98, 0.2)'
|
||||
: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
);
|
||||
}
|
||||
327
src/components/LandingUpcomingCities.tsx
Normal file
327
src/components/LandingUpcomingCities.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from './ui/button';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import Image592Traced from '../imports/Image592Traced-5025-559';
|
||||
|
||||
const upcomingCities = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Boston',
|
||||
country: 'USA',
|
||||
launchDate: 'Spring 2025',
|
||||
attractions: 65,
|
||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.',
|
||||
image: 'https://images.unsplash.com/photo-1568271667303-14b2a1a36da1?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Rome',
|
||||
country: 'Italy',
|
||||
launchDate: 'Summer 2025',
|
||||
attractions: 80,
|
||||
image: 'https://images.unsplash.com/photo-1552832230-c0197dd311b5?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Paris',
|
||||
country: 'France',
|
||||
launchDate: 'Fall 2025',
|
||||
attractions: 95,
|
||||
image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Dubai',
|
||||
country: 'UAE',
|
||||
launchDate: 'Winter 2025',
|
||||
attractions: 70,
|
||||
image: 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false,
|
||||
badge: 'New'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Tokyo',
|
||||
country: 'Japan',
|
||||
launchDate: 'Early 2026',
|
||||
attractions: 120,
|
||||
image: 'https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Sydney',
|
||||
country: 'Australia',
|
||||
launchDate: 'Spring 2026',
|
||||
attractions: 85,
|
||||
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'New York',
|
||||
country: 'USA',
|
||||
launchDate: 'Summer 2026',
|
||||
attractions: 150,
|
||||
image: 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false,
|
||||
badge: 'Most Requested'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Singapore',
|
||||
country: 'Singapore',
|
||||
launchDate: 'Fall 2026',
|
||||
attractions: 75,
|
||||
image: 'https://images.unsplash.com/photo-1525625293386-3f8f99389edd?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Amsterdam',
|
||||
country: 'Netherlands',
|
||||
launchDate: 'Winter 2026',
|
||||
attractions: 90,
|
||||
image: 'https://images.unsplash.com/photo-1534351590666-13e3e96b5017?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Barcelona',
|
||||
country: 'Spain',
|
||||
launchDate: 'Early 2027',
|
||||
attractions: 110,
|
||||
image: 'https://images.unsplash.com/photo-1583422409516-2895a77efded?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
}
|
||||
];
|
||||
|
||||
export function LandingUpcomingCities() {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [startX, setStartX] = useState(0);
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
const [showDragHint, setShowDragHint] = useState(false);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (!scrollContainerRef.current) return;
|
||||
// Only start dragging if not clicking on a button or interactive element
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('button') || target.closest('[role="button"]')) {
|
||||
return;
|
||||
}
|
||||
setIsDragging(true);
|
||||
setStartX(e.pageX - scrollContainerRef.current.offsetLeft);
|
||||
setScrollLeft(scrollContainerRef.current.scrollLeft);
|
||||
setShowDragHint(false);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsDragging(false);
|
||||
setShowDragHint(false);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!isDragging || !scrollContainerRef.current) return;
|
||||
e.preventDefault();
|
||||
const x = e.pageX - scrollContainerRef.current.offsetLeft;
|
||||
const walk = (x - startX) * 1.5; // Reduced multiplier for smoother movement
|
||||
scrollContainerRef.current.scrollLeft = scrollLeft - walk;
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isDragging) {
|
||||
setShowDragHint(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalMouseUp = () => setIsDragging(false);
|
||||
document.addEventListener('mouseup', handleGlobalMouseUp);
|
||||
return () => document.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-gray-50">
|
||||
{/* Header - Contained and aligned */}
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="mb-16">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4">
|
||||
Upcoming Cities
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 max-w-2xl">
|
||||
Here are lots of interesting destinations to visit, but don't be confused—they're already grouped by category.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cities Carousel - Aligned with header, extending to screen edge */}
|
||||
<div className="relative">
|
||||
{/* Drag Hint Pill */}
|
||||
{showDragHint && (
|
||||
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 z-20 bg-black/80 text-white px-4 py-2 rounded-full text-sm font-medium backdrop-blur-sm transition-all duration-300 pointer-events-none">
|
||||
Drag to scroll
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={`flex gap-6 overflow-x-auto scrollbar-hide pb-2 ${isDragging ? 'cursor-grabbing dragging select-none' : 'cursor-grab'}`}
|
||||
style={{
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none',
|
||||
scrollBehavior: isDragging ? 'auto' : 'smooth',
|
||||
paddingLeft: 'max(1rem, calc((100vw - 1280px) / 2 + 1rem))',
|
||||
paddingRight: '1rem'
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
>
|
||||
{upcomingCities.map((city) => (
|
||||
<div
|
||||
key={city.id}
|
||||
className="flex-shrink-0 w-72 md:w-80 group relative h-[420px] rounded-3xl overflow-hidden shadow-lg hover:shadow-xl transition-all duration-500"
|
||||
>
|
||||
{/* Background - Either solid color or image */}
|
||||
{city.showHoverState ? (
|
||||
// Boston card with image background and same layout as other cards
|
||||
<>
|
||||
<ImageWithFallback
|
||||
src={city.image!}
|
||||
alt={city.name}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
|
||||
{/* Dark overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent group-hover:from-black/80 transition-all duration-500" />
|
||||
|
||||
{/* City name overlay - matching Rome card layout */}
|
||||
<div className="absolute bottom-6 left-6 right-6 text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.name}</h3>
|
||||
<div className="flex items-center justify-between text-sm text-white/80">
|
||||
<span>{city.country}</span>
|
||||
<span>{city.launchDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover state overlay - same as other cards */}
|
||||
<div className="absolute inset-0 bg-warm-coral/90 opacity-0 group-hover:opacity-100 transition-all duration-500 flex items-center justify-center">
|
||||
<div className="text-center text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.name}</h3>
|
||||
<p className="text-white/90 mb-4">{city.attractions}+ attractions</p>
|
||||
<p className="text-sm text-white/80 mb-6">Coming {city.launchDate}</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-white/20 hover:bg-white/30 text-white border-white/30 hover:border-white/50 backdrop-blur-sm"
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('Notify Me button clicked');
|
||||
}}
|
||||
>
|
||||
Notify Me
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// Image background for other cards
|
||||
<>
|
||||
<ImageWithFallback
|
||||
src={city.image!}
|
||||
alt={city.name}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
|
||||
{/* Dark overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent group-hover:from-black/80 transition-all duration-500" />
|
||||
|
||||
{/* Badge (if present) */}
|
||||
{city.badge && (
|
||||
<div className="absolute top-4 right-4 bg-white text-gray-900 px-3 py-1 rounded-full text-sm font-medium shadow-lg">
|
||||
{city.badge}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* City name overlay */}
|
||||
<div className="absolute bottom-6 left-6 right-6 text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.name}</h3>
|
||||
<div className="flex items-center justify-between text-sm text-white/80">
|
||||
<span>{city.country}</span>
|
||||
<span>{city.launchDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover state overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/90 to-secondary/90 opacity-0 group-hover:opacity-100 transition-all duration-500 flex items-center justify-center">
|
||||
<div className="text-center text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.name}</h3>
|
||||
<p className="text-white/90 mb-4">{city.attractions}+ attractions</p>
|
||||
<p className="text-sm text-white/80 mb-6">Coming {city.launchDate}</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-white/20 hover:bg-white/30 text-white border-white/30 hover:border-white/50 backdrop-blur-sm"
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('Notify Me button clicked');
|
||||
}}
|
||||
>
|
||||
Notify Me
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom CTA - Contained */}
|
||||
<div className="container mx-auto px-4">
|
||||
|
||||
</div>
|
||||
|
||||
{/* Global Offices Section */}
|
||||
<div className="container mx-auto px-4 mt-20">
|
||||
{/* Global Presence Section - Exact Screenshot Recreation */}
|
||||
|
||||
</div>
|
||||
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.scrollbar-hide.dragging {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
`
|
||||
}} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
358
src/components/LandingVarietyOfAdventures.tsx
Normal file
358
src/components/LandingVarietyOfAdventures.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
import { Coffee, Palette, Trees, UtensilsCrossed, Music, Building2, Ship, ShoppingBag } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { useState } from 'react';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
|
||||
export function LandingVarietyOfAdventures() {
|
||||
const [hoveredCategory, setHoveredCategory] = useState<string | null>(null);
|
||||
|
||||
const melbourneCategories = [
|
||||
{
|
||||
id: 'street-art',
|
||||
title: 'Street Art & Laneways',
|
||||
tourCount: '12+ tours',
|
||||
icon: Palette,
|
||||
image: 'https://images.unsplash.com/photo-1613910774524-0651750373ec?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBzdHJlZXQlMjBhcnQlMjBncmFmZml0aSUyMGxhbmV3YXlzfGVufDF8fHx8MTc1NjEwNTYwOHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
attractions: [
|
||||
{
|
||||
name: 'Hosier Lane',
|
||||
image: 'https://images.unsplash.com/photo-1582076197789-5c2af0bb51fd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxob3NpZXIlMjBsYW5lJTIwbWVsYm91cm5lJTIwc3RyZWV0JTIwYXJ0fGVufDF8fHx8MTc1NjEwNjExMnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
},
|
||||
{
|
||||
name: 'AC/DC Lane',
|
||||
image: 'https://images.unsplash.com/photo-1735704197205-823cfd615e13?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhYyUyMGRjJTIwbGFuZSUyMG1lbGJvdXJuZSUyMGdyYWZmaXRpfGVufDF8fHx8MTc1NjEwNjExN3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'coffee-culture',
|
||||
title: 'Coffee Culture',
|
||||
tourCount: '8+ experiences',
|
||||
icon: Coffee,
|
||||
image: 'https://images.unsplash.com/photo-1681745623555-efc392301d6d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjb2ZmZWUlMjBjdWx0dXJlJTIwY2FmZSUyMGJhcmlzdGF8ZW58MXx8fHwxNzU2MTA1NjEyfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
attractions: [
|
||||
{
|
||||
name: 'Degraves Street',
|
||||
image: 'https://images.unsplash.com/photo-1686052183140-088f1bd9a49b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxkZWdyYXZlcyUyMHN0cmVldCUyMG1lbGJvdXJuZSUyMGNhZmV8ZW58MXx8fHwxNzU2MTA2MTIzfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
},
|
||||
{
|
||||
name: 'Centre Place',
|
||||
image: 'https://images.unsplash.com/photo-1583569695977-1e5758f793d1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjZW50cmUlMjBwbGFjZSUyMG1lbGJvdXJuZSUyMGNvZmZlZSUyMGxhbmV3YXl8ZW58MXx8fHwxNzU2MTA2MTI3fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'gardens-parks',
|
||||
title: 'Gardens & Parks',
|
||||
tourCount: '6+ nature spots',
|
||||
icon: Trees,
|
||||
image: 'https://images.unsplash.com/photo-1639481326289-1efe7cbfbfe5?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zJTIwbmF0dXJlfGVufDF8fHx8MTc1NjEwNTYxNnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
attractions: [
|
||||
{
|
||||
name: 'Royal Botanic Gardens',
|
||||
image: 'https://images.unsplash.com/photo-1670027537688-77def132d556?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zJTIwbWVsYm91cm5lJTIwbGFrZXxlbnwxfHx8fDE3NTYxMDYxMzN8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
},
|
||||
{
|
||||
name: 'Fitzroy Gardens',
|
||||
image: 'https://images.unsplash.com/photo-1735605918618-0193db0a30af?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxmaXR6cm95JTIwZ2FyZGVucyUyMG1lbGJvdXJuZSUyMGNvbnNlcnZhdG9yeXxlbnwxfHx8fDE3NTYxMDYxMzl8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'food-markets',
|
||||
title: 'Food & Markets',
|
||||
tourCount: '10+ food spots',
|
||||
icon: UtensilsCrossed,
|
||||
image: 'https://images.unsplash.com/photo-1656177796132-3dab16b30652?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0JTIwZm9vZHxlbnwxfHx8fDE3NTYxMDU2MTl8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
attractions: [
|
||||
{
|
||||
name: 'Queen Victoria Market',
|
||||
image: 'https://images.unsplash.com/photo-1708903965305-f8439248cebd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0JTIwbWVsYm91cm5lJTIwZm9vZCUyMHN0YWxsc3xlbnwxfHx8fDE3NTYxMDYxNDV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
},
|
||||
{
|
||||
name: 'South Melbourne Market',
|
||||
image: 'https://images.unsplash.com/photo-1749229964993-f802d5203142?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzb3V0aCUyMG1lbGJvdXJuZSUyMG1hcmtldCUyMGZvb2QlMjB2ZW5kb3JzfGVufDF8fHx8MTc1NjEwNjE1MXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'music-entertainment',
|
||||
title: 'Music & Entertainment',
|
||||
tourCount: '15+ venues',
|
||||
icon: Music,
|
||||
image: 'https://images.unsplash.com/photo-1684679106461-dae134df8da6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBtdXNpYyUyMGxpdmUlMjBjb25jZXJ0JTIwdmVudWV8ZW58MXx8fHwxNzU2MTA1NjI1fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
attractions: [
|
||||
{
|
||||
name: 'Princess Theatre',
|
||||
image: 'https://images.unsplash.com/photo-1709063370226-1369f8d26069?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxwcmluY2VzcyUyMHRoZWF0cmUlMjBtZWxib3VybmUlMjBoaXN0b3JpYyUyMHZlbnVlfGVufDF8fHx8MTc1NjEwNjE1N3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
},
|
||||
{
|
||||
name: 'Rod Laver Arena',
|
||||
image: 'https://images.unsplash.com/photo-1684679106461-dae134df8da6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBtdXNpYyUyMGxpdmUlMjBjb25jZXJ0JTIwdmVudWV8ZW58MXx8fHwxNzU2MTA1NjI1fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'architecture',
|
||||
title: 'Historic Architecture',
|
||||
tourCount: '9+ heritage sites',
|
||||
icon: Building2,
|
||||
image: 'https://images.unsplash.com/photo-1719447001523-7474ec81ef80?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBhcmNoaXRlY3R1cmUlMjBoaXN0b3JpYyUyMGJ1aWxkaW5nc3xlbnwxfHx8fDE3NTYxMDU2Mjl8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
attractions: [
|
||||
{
|
||||
name: 'Block Arcade',
|
||||
image: 'https://images.unsplash.com/photo-1695657678988-5bd451215eb4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxibG9jayUyMGFyY2FkZSUyMG1lbGJvdXJuZSUyMGhlcml0YWdlJTIwc2hvcHBpbmd8ZW58MXx8fHwxNzU2MTA2MTYxfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
},
|
||||
{
|
||||
name: 'St Paul\'s Cathedral',
|
||||
image: 'https://images.unsplash.com/photo-1719447001523-7474ec81ef80?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBhcmNoaXRlY3R1cmUlMjBoaXN0b3JpYyUyMGJ1aWxkaW5nc3xlbnwxfHx8fDE3NTYxMDU2Mjl8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'river-cruises',
|
||||
title: 'River & Cruises',
|
||||
tourCount: '5+ water experiences',
|
||||
icon: Ship,
|
||||
image: 'https://images.unsplash.com/photo-1722943661451-1a439d092ff2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByaXZlcnNpZGUlMjB5YXJyYSUyMHJpdmVyJTIwY3J1aXNlfGVufDF8fHx8MTc1NjEwNTYzM3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
attractions: [
|
||||
{
|
||||
name: 'Yarra River Cruise',
|
||||
image: 'https://images.unsplash.com/photo-1668376212180-d4c25f929ce4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx5YXJyYSUyMHJpdmVyJTIwbWVsYm91cm5lJTIwY3J1aXNlJTIwYm9hdHN8ZW58MXx8fHwxNzU2MTA2MTY1fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
},
|
||||
{
|
||||
name: 'Southbank Promenade',
|
||||
image: 'https://images.unsplash.com/photo-1722943661451-1a439d092ff2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByaXZlcnNpZGUlMjB5YXJyYSUyMHJpdmVyJTIwY3J1aXNlfGVufDF8fHx8MTc1NjEwNTYzM3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'shopping',
|
||||
title: 'Shopping & Style',
|
||||
tourCount: '7+ shopping areas',
|
||||
icon: ShoppingBag,
|
||||
image: 'https://images.unsplash.com/photo-1744357725934-12140170ebf3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBzaG9wcGluZyUyMGNvbGxpbnMlMjBzdHJlZXQlMjBib3V0aXF1ZXxlbnwxfHx8fDE3NTYxMDU2NDV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
attractions: [
|
||||
{
|
||||
name: 'Collins Street',
|
||||
image: 'https://images.unsplash.com/photo-1583569695977-1e5758f793d1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjb2xsaW5zJTIwc3RyZWV0JTIwbWVsYm91cm5lJTIwYm91dGlxdWUlMjBzaG9wcGluZ3xlbnwxfHx8fDE3NTYxMDYxNjl8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
},
|
||||
{
|
||||
name: 'Chapel Street',
|
||||
image: 'https://images.unsplash.com/photo-1744357725934-12140170ebf3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBzaG9wcGluZyUyMGNvbGxpbnMlMjBzdHJlZXQlMjBib3V0aXF1ZXxlbnwxfHx8fDE3NTYxMDU2NDV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Create extended array for seamless infinite scroll
|
||||
const extendedCategories = [...melbourneCategories, ...melbourneCategories, ...melbourneCategories];
|
||||
|
||||
return (
|
||||
<section className="py-20 lg:py-28 bg-white overflow-hidden">
|
||||
<div className="container mx-auto px-4">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-16 max-w-4xl mx-auto">
|
||||
<motion.h2
|
||||
className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight text-foreground mb-6"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
>
|
||||
<span className="font-light">A Melbourne</span>{' '}
|
||||
<span className="font-bold text-primary italic">Experience</span>{' '}
|
||||
<span className="font-light">for Every Traveller</span>
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
className="font-poppins text-xl leading-relaxed font-normal text-gray-600"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
>
|
||||
From iconic laneways and world-class coffee to stunning gardens and vibrant markets,
|
||||
discover Melbourne's unique character through curated experiences that showcase the city's soul.
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* Full-Width Horizontal Scrolling Carousel */}
|
||||
<div className="mb-16 relative">
|
||||
{/* Carousel Container - Full Width */}
|
||||
<div className="relative w-full overflow-hidden">
|
||||
{/* Scrolling Track */}
|
||||
<motion.div
|
||||
className="horizontal-scroll-track flex items-center gap-8 py-8"
|
||||
style={{
|
||||
width: 'max-content',
|
||||
animation: 'scrollHorizontal 80s linear infinite'
|
||||
}}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.4 }}
|
||||
>
|
||||
{/* Adventure Category Cards */}
|
||||
{extendedCategories.map((category, index) => (
|
||||
<motion.div
|
||||
key={`${category.id}-${index}`}
|
||||
className="flex-shrink-0 w-80 h-96 group cursor-pointer relative"
|
||||
onMouseEnter={() => setHoveredCategory(`${category.id}-${index}`)}
|
||||
onMouseLeave={() => setHoveredCategory(null)}
|
||||
whileHover={{ scale: 1.02, y: -4 }}
|
||||
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
>
|
||||
{/* Card Content - New Design */}
|
||||
<div className="relative rounded-3xl overflow-hidden h-full shadow-lg hover:shadow-2xl transition-all duration-500">
|
||||
{/* Full Background Image */}
|
||||
<div className="absolute inset-0">
|
||||
<ImageWithFallback
|
||||
src={category.image}
|
||||
alt={category.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
{/* Dark Overlay */}
|
||||
<div className="absolute inset-0 bg-black/30" />
|
||||
</div>
|
||||
|
||||
{/* Bottom Content Card */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6">
|
||||
<motion.div
|
||||
className="bg-white/95 backdrop-blur-sm rounded-2xl p-4 border border-white/20"
|
||||
whileHover={{ y: -2 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Text Content */}
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-1">
|
||||
{category.title}
|
||||
</h3>
|
||||
<p className="text-base font-medium text-gray-600">
|
||||
{category.tourCount}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Icon */}
|
||||
<motion.div
|
||||
className="w-12 h-12 bg-warm-coral rounded-xl flex items-center justify-center flex-shrink-0"
|
||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<category.icon className="w-6 h-6 text-white" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Subtle Animation Effect */}
|
||||
<div className="absolute -bottom-2 -right-2 w-16 h-16 bg-warm-coral/10 rounded-full blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
</div>
|
||||
|
||||
{/* Popup Card - Attractions */}
|
||||
<AnimatePresence>
|
||||
{hoveredCategory === `${category.id}-${index}` && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
layout: { duration: 0.2 }
|
||||
}}
|
||||
className="absolute top-0 left-0 w-full h-full z-20 pointer-events-none"
|
||||
>
|
||||
{/* Popup Content */}
|
||||
<div className="relative w-full h-full bg-white/95 backdrop-blur-lg rounded-3xl border border-white/60 shadow-2xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-white/40">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-warm-coral/20 rounded-xl flex items-center justify-center">
|
||||
<category.icon className="w-5 h-5 text-warm-coral" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900">{category.title}</h4>
|
||||
<p className="text-sm text-warm-coral/80">Featured Attractions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Attractions Grid */}
|
||||
<div className="p-6 space-y-4">
|
||||
{category.attractions.map((attraction, idx) => (
|
||||
<motion.div
|
||||
key={attraction.name}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay: idx * 0.1,
|
||||
ease: [0.25, 0.1, 0.25, 1]
|
||||
}}
|
||||
className="flex items-center gap-4 p-3 rounded-2xl bg-white/60 backdrop-blur-sm border border-white/40 hover:bg-white/80 transition-all duration-300"
|
||||
>
|
||||
{/* Attraction Image */}
|
||||
<div className="w-16 h-16 flex-shrink-0 rounded-xl overflow-hidden">
|
||||
<ImageWithFallback
|
||||
src={attraction.image}
|
||||
alt={attraction.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Attraction Info */}
|
||||
<div className="flex-1">
|
||||
<h5 className="font-semibold text-gray-900 mb-1">
|
||||
{attraction.name}
|
||||
</h5>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-1.5 h-1.5 bg-warm-coral rounded-full" />
|
||||
<span className="text-sm text-warm-coral/80">
|
||||
Included in Pass
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-6 pt-0">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
+ Many more attractions included
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Gradient Fade Edges */}
|
||||
<div className="absolute left-0 top-0 bottom-0 w-32 bg-white/80 pointer-events-none z-10" />
|
||||
<div className="absolute right-0 top-0 bottom-0 w-32 bg-white/80 pointer-events-none z-10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Button */}
|
||||
<motion.div
|
||||
className="text-center"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.6, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
>
|
||||
<Button
|
||||
withShine={true}
|
||||
size="xl"
|
||||
className="h-16 rounded-full text-lg px-8"
|
||||
>
|
||||
Get A City Card And Start Exploring
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
173
src/components/LandingWhyChooseCityCards.tsx
Normal file
173
src/components/LandingWhyChooseCityCards.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { motion } from 'motion/react';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
|
||||
export function LandingWhyChooseCityCards() {
|
||||
return (
|
||||
<section className="py-20 bg-gradient-to-b from-white to-gray-50/30 overflow-hidden">
|
||||
<div className="container mx-auto px-4">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center gap-2 bg-primary/10 px-4 py-2 rounded-full mb-6">
|
||||
<div className="w-2 h-2 bg-primary rounded-full"></div>
|
||||
<span className="font-poppins text-sm font-medium text-primary">
|
||||
Smart Savings
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight text-foreground mb-4">
|
||||
<span className="font-light">Save</span>{' '}
|
||||
<span className="font-bold text-primary italic">Big</span>{' '}
|
||||
<span className="font-normal">on attractions</span>
|
||||
</h2>
|
||||
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-700 max-w-3xl mx-auto">
|
||||
Why pay $350+ buying tickets individually when Melbourne City Card gives you 40+ attractions for as little as $199?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Polaroid Cards Layout */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
|
||||
|
||||
{/* Melbourne Card */}
|
||||
<motion.div
|
||||
className="relative"
|
||||
initial={{ opacity: 0, y: 30, rotate: -2 }}
|
||||
whileInView={{ opacity: 1, y: 0, rotate: -1 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div className="bg-white p-4 shadow-xl transform rotate-1 hover:rotate-0 transition-transform duration-300">
|
||||
{/* Image */}
|
||||
<div className="aspect-[4/3] overflow-hidden mb-4">
|
||||
<ImageWithFallback
|
||||
src="https://images.unsplash.com/photo-1514395462725-fb4566210144?w=600&h=400&fit=crop"
|
||||
alt="Melbourne cityscape"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Polaroid Caption */}
|
||||
<div className="text-center">
|
||||
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-semibold text-foreground mb-2">Melbourne</h3>
|
||||
<div className="space-y-2 font-poppins">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-base font-normal text-gray-700">Individual tickets:</span>
|
||||
<span className="text-base font-normal text-foreground line-through">$350+</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-base font-normal text-gray-700">City Card:</span>
|
||||
<span className="text-2xl font-bold text-primary">$199</span>
|
||||
</div>
|
||||
<div className="text-center pt-2">
|
||||
<span className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm font-poppins font-medium">
|
||||
Save $151+
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute -top-2 -right-2 w-6 h-6 bg-yellow-400 rounded-full shadow-lg transform rotate-12"></div>
|
||||
<div className="absolute -bottom-3 -left-3 w-4 h-4 bg-blue-400 rounded-full shadow-lg"></div>
|
||||
</motion.div>
|
||||
|
||||
{/* Sydney Card */}
|
||||
<motion.div
|
||||
className="relative"
|
||||
initial={{ opacity: 0, y: 30, rotate: 2 }}
|
||||
whileInView={{ opacity: 1, y: 0, rotate: 1 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut", delay: 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div className="bg-white p-4 shadow-xl transform -rotate-1 hover:rotate-0 transition-transform duration-300">
|
||||
{/* Image */}
|
||||
<div className="aspect-[4/3] overflow-hidden mb-4">
|
||||
<ImageWithFallback
|
||||
src="https://images.unsplash.com/photo-1624138784614-87fd1b6528f8?w=600&h=400&fit=crop"
|
||||
alt="Sydney Opera House and Harbour Bridge"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Polaroid Caption */}
|
||||
<div className="text-center">
|
||||
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-semibold text-foreground mb-2">Sydney</h3>
|
||||
<div className="space-y-2 font-poppins">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-base font-normal text-gray-700">Individual tickets:</span>
|
||||
<span className="text-base font-normal text-foreground line-through">$420+</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-base font-normal text-gray-700">City Card:</span>
|
||||
<span className="text-2xl font-bold text-primary">$249</span>
|
||||
</div>
|
||||
<div className="text-center pt-2">
|
||||
<span className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm font-poppins font-medium">
|
||||
Save $171+
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute -top-3 -left-2 w-5 h-5 bg-pink-400 rounded-full shadow-lg transform -rotate-12"></div>
|
||||
<div className="absolute -bottom-2 -right-3 w-3 h-3 bg-green-400 rounded-full shadow-lg"></div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Features */}
|
||||
<motion.div
|
||||
className="mt-16 text-center"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div className="bg-primary/5 rounded-3xl p-8 border border-gray-100">
|
||||
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-semibold text-foreground mb-6">
|
||||
<span className="font-normal">Why</span>{' '}
|
||||
<span className="text-primary">CityCards</span>?
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-green-100 to-green-50 rounded-xl flex items-center justify-center mx-auto mb-3 shadow-lg border border-green-200/50">
|
||||
<span className="text-3xl filter drop-shadow-sm">💰</span>
|
||||
</div>
|
||||
<h4 className="font-poppins text-lg leading-snug font-medium text-foreground mb-2">Massive Savings</h4>
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal text-gray-500">Save up to 50% compared to individual attraction tickets</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-yellow-100 to-orange-50 rounded-xl flex items-center justify-center mx-auto mb-3 shadow-lg border border-yellow-200/50">
|
||||
<span className="text-3xl filter drop-shadow-sm">⚡</span>
|
||||
</div>
|
||||
<h4 className="font-poppins text-lg leading-snug font-medium text-foreground mb-2">Skip the Lines</h4>
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal text-gray-500">Fast-track entry to popular attractions and experiences</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-blue-100 to-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-3 shadow-lg border border-blue-200/50">
|
||||
<span className="text-3xl filter drop-shadow-sm">📱</span>
|
||||
</div>
|
||||
<h4 className="font-poppins text-lg leading-snug font-medium text-foreground mb-2">Digital Convenience</h4>
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal text-gray-500">Everything on your phone - no physical tickets needed</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.button
|
||||
className="mt-8 bg-primary text-white py-4 px-12 rounded-full font-poppins font-semibold text-base hover:shadow-lg hover:shadow-primary/25 transition-all duration-200"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
Start Saving Today
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -172,12 +172,12 @@ export function MelbourneAttractions() {
|
||||
/>
|
||||
|
||||
{/* Rating Badge */}
|
||||
<div className="absolute top-4 right-4 bg-white/95 backdrop-blur-sm rounded-full px-3 py-1.5 flex items-center gap-1 shadow-lg z-10">
|
||||
{/* <div className="absolute top-4 right-4 bg-white/95 backdrop-blur-sm rounded-full px-3 py-1.5 flex items-center gap-1 shadow-lg z-10">
|
||||
<div className="w-4 h-4 bg-gradient-to-r from-yellow-400 to-yellow-500 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-xs">★</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-900">{attraction.rating}</span>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Front Content - Clean Title & Location */}
|
||||
<div className="absolute bottom-0 left-0 right-0">
|
||||
|
||||
@@ -137,39 +137,6 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Card Options Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex justify-center mb-12"
|
||||
>
|
||||
<div className="flex bg-white rounded-2xl p-2 shadow-lg border border-gray-100 gap-2">
|
||||
{cardOptions.map((card) => (
|
||||
<motion.button
|
||||
key={card.id}
|
||||
onClick={() => setSelectedCard(card.id)}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className={`px-8 py-4 rounded-xl font-medium transition-all duration-300 relative overflow-hidden ${
|
||||
selectedCard === card.id
|
||||
? `bg-gradient-to-r ${card.color} text-white shadow-lg`
|
||||
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{card.popular && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-yellow-400 rounded-full"></div>
|
||||
)}
|
||||
<div className="text-center">
|
||||
<div className="font-semibold text-lg">{card.name}</div>
|
||||
<div className="text-sm opacity-90 mt-1">{card.priceRange}</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Comparison Table */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
@@ -181,15 +148,15 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
|
||||
{/* Table Header */}
|
||||
<div className="bg-gradient-to-r from-gray-50 to-gray-100 px-8 py-6">
|
||||
<div className="grid grid-cols-3 gap-8 items-center">
|
||||
<div className="font-merchant text-xl font-semibold text-gray-900">
|
||||
<div className="font-poppins text-xl font-semibold text-gray-900">
|
||||
Features
|
||||
</div>
|
||||
{cardOptions.map((card) => (
|
||||
<div key={card.id} className="text-center">
|
||||
<div className={`inline-block px-6 py-3 rounded-2xl bg-gradient-to-r ${card.color} text-white mb-2`}>
|
||||
<div className="font-semibold text-lg">{card.name}</div>
|
||||
<div className="mb-2">
|
||||
<div className="font-poppins font-semibold text-2xl" style={{ color: '#F95F62' }}>{card.name}</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 max-w-xs mx-auto leading-relaxed">
|
||||
<div className="font-poppins text-sm text-gray-600 max-w-xs mx-auto leading-relaxed">
|
||||
{card.subtitle}
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,7 +205,8 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
|
||||
</div>
|
||||
<Button
|
||||
withShine={true}
|
||||
className={`w-full h-14 rounded-2xl text-white font-semibold text-lg bg-gradient-to-r ${card.color} hover:scale-105 transition-all duration-300 shadow-lg hover:shadow-xl`}
|
||||
className="w-full h-14 rounded-2xl text-white font-semibold text-lg hover:scale-105 transition-all duration-300 shadow-lg hover:shadow-xl"
|
||||
style={{ backgroundColor: '#F95F62' }}
|
||||
onClick={onCheckoutClick}
|
||||
>
|
||||
Buy {card.name}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { motion } from 'motion/react';
|
||||
import { motion, useAnimationControls } from 'motion/react';
|
||||
import { Button } from './ui/button';
|
||||
import { ArrowRight, Calendar, Thermometer, Eye } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Navbar from './Navbar';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { CitySubmenu } from './CitySubmenu';
|
||||
import { MelbourneAttractions } from './MelbourneAttractions';
|
||||
import { MelbourneCardComparison } from './MelbourneCardComparison';
|
||||
@@ -10,12 +13,237 @@ import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { MobileAppPromotion } from './MobileAppPromotion';
|
||||
import { MelbourneFAQ } from './MelbourneFAQ';
|
||||
import { Footer } from './Footer';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Hero Banner Carousel Component
|
||||
function HeroBannerCarousel({ onCheckoutClick, onPassesClick, onEsimsClick, onHotelDiscountsClick }: {
|
||||
onCheckoutClick?: () => void;
|
||||
onPassesClick?: () => void;
|
||||
onEsimsClick?: () => void;
|
||||
onHotelDiscountsClick?: () => void;
|
||||
}) {
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
const slides = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Discover",
|
||||
highlight: "Melbourne",
|
||||
subtitle: "Ultimate Guide to Iconic City",
|
||||
description: "From Flinders Street to St Kilda Beach: explore the best of Melbourne's landmarks, culture, food and more!",
|
||||
image: "https://images.unsplash.com/photo-1757470238279-0e9f331d02c9?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBza3lsaW5lJTIwc3Vuc2V0fGVufDF8fHx8MTc2MDUwOTIyMHww&ixlib=rb-4.1.0&q=80&w=1080",
|
||||
cta: "Get Started",
|
||||
onClick: onCheckoutClick
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Unlock",
|
||||
highlight: "City Passes",
|
||||
subtitle: "Save More, Experience More",
|
||||
description: "Get unlimited access to top attractions with our flexible city passes. Choose from 1, 2, 3, or 5-day options!",
|
||||
image: "https://images.unsplash.com/photo-1743441914096-e8f9aaded6f8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjaXR5JTIwYXR0cmFjdGlvbiUyMHBhc3N8ZW58MXx8fHwxNzYwNTA5MjIxfDA&ixlib=rb-4.1.0&q=80&w=1080",
|
||||
cta: "Explore Passes",
|
||||
onClick: onPassesClick
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Stay",
|
||||
highlight: "Connected",
|
||||
subtitle: "Travel eSIMs for Every Journey",
|
||||
description: "Never lose touch while traveling. Get instant data connectivity in over 190 countries with our eSIM solutions!",
|
||||
image: "https://images.unsplash.com/photo-1755286218783-5b8334109336?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtb2JpbGUlMjBwaG9uZSUyMHJvYW1pbmd8ZW58MXx8fHwxNzYwNTA5MjIxfDA&ixlib=rb-4.1.0&q=80&w=1080",
|
||||
cta: "Get eSIM",
|
||||
onClick: onEsimsClick
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Exclusive",
|
||||
highlight: "Hotel Deals",
|
||||
subtitle: "Luxury Stays at Best Prices",
|
||||
description: "Book premium hotels at unbeatable rates. Enjoy exclusive discounts on handpicked accommodations worldwide!",
|
||||
image: "https://images.unsplash.com/photo-1634041441461-a1789d008830?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsdXh1cnklMjBob3RlbCUyMGV4dGVyaW9yfGVufDF8fHx8MTc2MDQ5NzY1MXww&ixlib=rb-4.1.0&q=80&w=1080",
|
||||
cta: "View Hotels",
|
||||
onClick: onHotelDiscountsClick
|
||||
}
|
||||
];
|
||||
|
||||
// Auto-scroll effect
|
||||
useEffect(() => {
|
||||
if (isPaused) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentSlide((prev) => (prev + 1) % slides.length);
|
||||
}, 5000); // Change slide every 5 seconds
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isPaused, slides.length]);
|
||||
|
||||
const goToSlide = (index: number) => {
|
||||
setCurrentSlide(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full min-h-screen overflow-hidden"
|
||||
onMouseEnter={() => setIsPaused(true)}
|
||||
onMouseLeave={() => setIsPaused(false)}
|
||||
>
|
||||
{/* Slides */}
|
||||
{slides.map((slide, index) => {
|
||||
return (
|
||||
<motion.div
|
||||
key={slide.id}
|
||||
className="absolute inset-0 w-full h-full flex"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: currentSlide === index ? 1 : 0,
|
||||
zIndex: currentSlide === index ? 1 : 0
|
||||
}}
|
||||
transition={{ duration: 1, ease: "easeInOut" }}
|
||||
>
|
||||
{/* Left Side - Content Panel (60% width) */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
animate={{
|
||||
opacity: currentSlide === index ? 1 : 0,
|
||||
x: currentSlide === index ? 0 : -30
|
||||
}}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
className="relative w-full md:w-[60%] h-full flex items-center"
|
||||
style={{ backgroundColor: '#FFF5F5' }}
|
||||
>
|
||||
<div className="px-6 sm:px-8 md:px-10 lg:px-16 xl:px-20 py-8 w-full">
|
||||
<h1 className="font-poppins leading-tight mb-4 lg:mb-6">
|
||||
<div className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl xl:text-7xl mb-2">
|
||||
<span className="font-light" style={{ color: '#1F2937' }}>{slide.title} </span>
|
||||
<span className="font-bold text-white px-3 py-1.5 md:px-4 md:py-2 inline-block" style={{ backgroundColor: '#F95F62' }}>
|
||||
{slide.highlight}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold mt-3 md:mt-4" style={{ color: '#1F2937' }}>
|
||||
{slide.subtitle}
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<p className="font-poppins text-base md:text-lg lg:text-xl leading-relaxed font-normal mb-6 lg:mb-8 max-w-2xl" style={{ color: '#4B5563' }}>
|
||||
{slide.description}
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={slide.onClick}
|
||||
className="group px-6 py-3 md:px-8 md:py-4 rounded-full flex items-center gap-3 transition-all duration-300 hover:scale-105 shadow-lg hover:shadow-xl font-poppins font-semibold text-base text-white"
|
||||
style={{ backgroundColor: '#F95F62' }}
|
||||
>
|
||||
{slide.cta}
|
||||
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-300" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Side - Full Height Image (40% width) */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
animate={{
|
||||
opacity: currentSlide === index ? 1 : 0,
|
||||
x: currentSlide === index ? 0 : 30
|
||||
}}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
className="relative w-full md:w-[40%] h-full overflow-hidden"
|
||||
>
|
||||
<ImageWithFallback
|
||||
src={slide.image}
|
||||
alt={`${slide.highlight} - ${slide.subtitle}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
||||
{/* Subtle overlay for depth */}
|
||||
<div className="absolute inset-0 bg-gradient-to-l from-transparent to-black/10" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Slide Indicators - Bottom Center (All Screens) */}
|
||||
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-20 flex gap-3">
|
||||
{slides.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => goToSlide(index)}
|
||||
className="group"
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
>
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-300 ${
|
||||
currentSlide === index
|
||||
? 'w-12'
|
||||
: 'w-2 group-hover:opacity-75'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: currentSlide === index ? '#F95F62' : '#9CA3AF',
|
||||
opacity: currentSlide === index ? 1 : 0.5
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Numbered Slide Navigation - Bottom Left (Desktop) */}
|
||||
<div className="hidden md:block absolute bottom-8 left-8 lg:left-16 z-20">
|
||||
<div className="flex gap-6 lg:gap-8">
|
||||
{slides.map((slide, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => goToSlide(index)}
|
||||
className="group text-left transition-all duration-300"
|
||||
aria-label={`Go to slide ${index + 1}: ${slide.highlight}`}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Number with active indicator line */}
|
||||
<div className="relative">
|
||||
{currentSlide === index && (
|
||||
<div
|
||||
className="absolute -top-2 left-0 right-0 h-0.5 rounded-full transition-all duration-300"
|
||||
style={{ backgroundColor: '#F95F62' }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={`font-poppins font-semibold transition-all duration-300 ${
|
||||
currentSlide === index
|
||||
? 'text-2xl'
|
||||
: 'text-xl'
|
||||
}`}
|
||||
style={{
|
||||
color: currentSlide === index ? '#F95F62' : '#000000',
|
||||
opacity: currentSlide === index ? 1 : 0.6
|
||||
}}
|
||||
>
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<div className={`transition-all duration-300 ${
|
||||
currentSlide === index ? 'opacity-100' : 'opacity-70'
|
||||
}`}>
|
||||
<p className="font-poppins text-sm text-black/90 whitespace-nowrap max-w-[120px]">
|
||||
{slide.highlight}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface MelbournePageProps {
|
||||
onBackClick: () => void;
|
||||
onHomeClick: () => void;
|
||||
@@ -66,107 +294,23 @@ export function MelbournePage({
|
||||
user
|
||||
}: MelbournePageProps) {
|
||||
return (
|
||||
<Layout
|
||||
activeCity="Melbourne"
|
||||
showCitySubmenu={true}
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
>
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Navbar */}
|
||||
<Navbar
|
||||
activeCity="Melbourne"
|
||||
onCityChange={(city) => {
|
||||
if (city === 'Melbourne') {
|
||||
// Already on Melbourne page
|
||||
}
|
||||
}}
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
onPassesClick={onPassesClick}
|
||||
|
||||
{/* Hero Banner Carousel */}
|
||||
<HeroBannerCarousel
|
||||
onCheckoutClick={onCheckoutClick}
|
||||
onHomeClick={onHomeClick}
|
||||
onAttractionsClick={onAttractionsClick}
|
||||
onBlogsClick={onBlogsClick}
|
||||
onHowItWorksClick={onHowItWorksClick}
|
||||
onFAQClick={onFAQClick}
|
||||
onPrivacyPolicyClick={onPrivacyPolicyClick}
|
||||
onAboutUsClick={onAboutUsClick}
|
||||
onProfileClick={onProfileClick}
|
||||
onCityCardsClick={onCityCardsClick}
|
||||
onMagicItineraryClick={onMagicItineraryClick}
|
||||
onPostCardsClick={onPostCardsClick}
|
||||
onOffersClick={onOffersClick}
|
||||
onPassesClick={onPassesClick}
|
||||
onEsimsClick={onEsimsClick}
|
||||
onHotelDiscountsClick={onHotelDiscountsClick}
|
||||
currentPage={currentPage || "melbourne"}
|
||||
isUserSignedIn={!!user}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
{/* City Submenu */}
|
||||
<CitySubmenu
|
||||
onClose={() => {}} // Empty function since it's always shown
|
||||
currentPage="melbourne"
|
||||
onHomeClick={onHomeClick}
|
||||
onMelbourneClick={() => {}} // Already on Melbourne page
|
||||
onAttractionsClick={onAttractionsClick}
|
||||
onPassesClick={onPassesClick}
|
||||
onBlogsClick={onBlogsClick}
|
||||
onHowItWorksClick={onHowItWorksClick}
|
||||
/>
|
||||
|
||||
{/* Hero Section - Full Width */}
|
||||
<div
|
||||
className="relative w-full h-[85vh] bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.42), rgba(0, 0, 0, 0.42)), url('https://images.unsplash.com/photo-1573639571368-065819727a52?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx8fDE3NTczOTY3MDd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral')`
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-x-0 top-0 bottom-0 flex items-center justify-start pl-8 md:pl-16 lg:pl-24">
|
||||
<div className="text-left text-white px-4">
|
||||
<motion.h1
|
||||
className="font-poppins text-5xl md:text-6xl lg:text-7xl leading-tight mb-6"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 1, delay: 0.3 }}
|
||||
>
|
||||
<span className="font-light">Discover</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-yellow-300 to-orange-400 bg-clip-text text-transparent">
|
||||
Melbourne
|
||||
</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
className="font-poppins text-xl leading-relaxed font-light mb-4 max-w-3xl"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
>
|
||||
Explore Melbourne's vibrant culture, world-class dining, and iconic attractions
|
||||
</motion.p>
|
||||
|
||||
<motion.p
|
||||
className="font-poppins text-xl leading-relaxed font-normal mb-8 text-yellow-300"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.7 }}
|
||||
>
|
||||
Save up to 50% on all attractions
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.9 }}
|
||||
>
|
||||
<Button
|
||||
withShine={true}
|
||||
className="font-poppins font-semibold h-[70px] min-w-[280px] px-12 py-6 rounded-full text-white text-xl hover:scale-105 transition-transform duration-300"
|
||||
style={{ backgroundColor: '#F95F62' }}
|
||||
onClick={onCheckoutClick}
|
||||
>
|
||||
BUY A CARD
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<main>
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
@@ -252,5 +396,6 @@ export function MelbournePage({
|
||||
currentPage="melbourne"
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -125,7 +125,7 @@ export function MelbourneTourOverview() {
|
||||
className="bg-white rounded-3xl p-8 shadow-lg border border-gray-100"
|
||||
>
|
||||
<h3 className="font-merchant text-2xl md:text-3xl font-semibold text-gray-900 mb-6">
|
||||
What's included
|
||||
What Will You Experience
|
||||
</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
@@ -253,19 +253,6 @@ export function MelbourneTourOverview() {
|
||||
viewport={{ once: true }}
|
||||
className="mt-8 pt-6 border-t border-gray-100"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 mb-1">Starting from</div>
|
||||
<div className="text-2xl font-bold text-gray-900">$89</div>
|
||||
</div>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="bg-gradient-to-r from-primary to-secondary text-white px-6 py-3 rounded-xl font-medium shadow-lg hover:shadow-xl transition-all duration-300"
|
||||
>
|
||||
View All Tours
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -12,23 +13,6 @@ interface NavbarProps {
|
||||
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;
|
||||
}
|
||||
@@ -39,6 +23,7 @@ interface DropdownItem {
|
||||
icon?: React.ReactNode;
|
||||
action?: () => void;
|
||||
badge?: string | number;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
interface CartItem {
|
||||
@@ -63,23 +48,6 @@ export default function Navbar({
|
||||
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) {
|
||||
@@ -88,12 +56,15 @@ export default function Navbar({
|
||||
const [activeLanguageDropdown, setActiveLanguageDropdown] = useState(false);
|
||||
const [activeCartDropdown, setActiveCartDropdown] = useState(false);
|
||||
const [activeUserDropdown, setActiveUserDropdown] = useState(false);
|
||||
const [activeProductsDropdown, setActiveProductsDropdown] = useState(false);
|
||||
const [activeExploreCardDropdown, setActiveExploreCardDropdown] = useState(false);
|
||||
|
||||
const languageRef = useRef<HTMLDivElement>(null);
|
||||
const cartRef = useRef<HTMLDivElement>(null);
|
||||
const userRef = useRef<HTMLDivElement>(null);
|
||||
const productsRef = useRef<HTMLDivElement>(null);
|
||||
const exploreCardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Languages available
|
||||
const languages: DropdownItem[] = [
|
||||
@@ -103,32 +74,37 @@ export default function Navbar({
|
||||
{ id: 'it', label: 'Italiano', icon: <span className="text-base">🇮🇹</span> },
|
||||
];
|
||||
|
||||
// Products dropdown items
|
||||
const productsItems: DropdownItem[] = [
|
||||
// Explore Card dropdown items with router navigation
|
||||
const exploreCardItems: DropdownItem[] = [
|
||||
{
|
||||
id: 'citycards',
|
||||
label: 'CityCards',
|
||||
action: onCityCardsClick
|
||||
path: '/citycards'
|
||||
},
|
||||
{
|
||||
id: 'magic-itinerary',
|
||||
label: 'Magic Itinerary',
|
||||
action: onMagicItineraryClick
|
||||
path: '/magic-itinerary'
|
||||
},
|
||||
{
|
||||
id: 'postcards',
|
||||
label: 'Post Cards',
|
||||
action: onPostCardsClick
|
||||
label: 'PostCards',
|
||||
path: '/postcards'
|
||||
},
|
||||
{
|
||||
id: 'offers',
|
||||
label: 'Offers',
|
||||
action: onOffersClick
|
||||
path: '/offers'
|
||||
},
|
||||
{
|
||||
id: 'esims',
|
||||
label: 'eSIMs',
|
||||
action: onEsimsClick
|
||||
path: '/esims'
|
||||
},
|
||||
{
|
||||
id: 'hotel-discounts',
|
||||
label: 'Hotel Discounts',
|
||||
path: '/hotel-discounts'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -179,7 +155,6 @@ export default function Navbar({
|
||||
// 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);
|
||||
@@ -190,8 +165,8 @@ export default function Navbar({
|
||||
if (userRef.current && !userRef.current.contains(event.target as Node)) {
|
||||
setActiveUserDropdown(false);
|
||||
}
|
||||
if (productsRef.current && !productsRef.current.contains(event.target as Node)) {
|
||||
setActiveProductsDropdown(false);
|
||||
if (exploreCardRef.current && !exploreCardRef.current.contains(event.target as Node)) {
|
||||
setActiveExploreCardDropdown(false);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
@@ -200,28 +175,19 @@ export default function Navbar({
|
||||
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;
|
||||
}
|
||||
const handleNavClick = (path: string) => {
|
||||
navigate(path);
|
||||
closeMobileMenu();
|
||||
};
|
||||
|
||||
const isNavItemActive = (action: string) => {
|
||||
if (action === 'about') {
|
||||
return currentPage === 'about-us';
|
||||
}
|
||||
return currentPage === action;
|
||||
const isNavItemActive = (path: string) => {
|
||||
return location.pathname === path;
|
||||
};
|
||||
|
||||
// Handle Explore Card dropdown item click
|
||||
const handleExploreCardItemClick = (path: string) => {
|
||||
navigate(path);
|
||||
setActiveExploreCardDropdown(false);
|
||||
};
|
||||
|
||||
// Calculate cart total
|
||||
@@ -230,6 +196,13 @@ export default function Navbar({
|
||||
return total + (price * item.quantity);
|
||||
}, 0);
|
||||
|
||||
// Navigation items with router paths
|
||||
const navigationItems = [
|
||||
{ label: 'How It Works', path: '/how-it-works' },
|
||||
{ label: 'Your Card', path: '/passes' },
|
||||
{ label: 'FAQ', path: '/comming-soon' }
|
||||
];
|
||||
|
||||
// Dropdown component with proper ref forwarding and glassmorphism
|
||||
const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(({
|
||||
isOpen,
|
||||
@@ -278,23 +251,27 @@ export default function Navbar({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Handle action first
|
||||
// Handle router navigation for items with paths
|
||||
if (item.path) {
|
||||
navigate(item.path);
|
||||
}
|
||||
|
||||
// Handle action if provided
|
||||
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)) {
|
||||
// Only close dropdown for actionable items
|
||||
if (item.id === 'checkout' || item.path || (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'
|
||||
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 }}
|
||||
@@ -302,10 +279,9 @@ export default function Navbar({
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
{item.icon}
|
||||
<span className={`text-sm font-medium ${
|
||||
item.id === 'checkout' ? 'text-white' :
|
||||
<span className={`text-sm font-medium ${item.id === 'checkout' ? 'text-white' :
|
||||
item.id === 'total' ? 'text-gray-900' : 'text-gray-700'
|
||||
}`}>{item.label}</span>
|
||||
}`}>{item.label}</span>
|
||||
</div>
|
||||
{item.badge && (
|
||||
<span className="bg-primary text-primary-foreground text-xs px-2 py-1 rounded-full">
|
||||
@@ -335,11 +311,10 @@ export default function Navbar({
|
||||
>
|
||||
<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'
|
||||
}`}
|
||||
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,
|
||||
@@ -354,31 +329,42 @@ export default function Navbar({
|
||||
className="flex items-center cursor-pointer flex-shrink-0"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => (window.location.href = "https://citycards-landing.wdiprojects.com/")}
|
||||
>
|
||||
<ImageWithFallback
|
||||
src={logoImage}
|
||||
alt="CityCards Logo"
|
||||
className="h-10 w-auto"
|
||||
/>
|
||||
<Link to="/">
|
||||
<ImageWithFallback
|
||||
src={logoImage}
|
||||
alt="CityCards Logo"
|
||||
className="h-10 w-auto"
|
||||
/>
|
||||
</Link>
|
||||
</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 }}
|
||||
<div className="absolute left-1/2 -translate-x-1/2 flex items-center gap-5">
|
||||
{/* Explore Card - First Item - No Dropdown */}
|
||||
<Dropdown
|
||||
ref={exploreCardRef}
|
||||
isOpen={activeExploreCardDropdown}
|
||||
onToggle={() => setActiveExploreCardDropdown(!activeExploreCardDropdown)}
|
||||
items={exploreCardItems}
|
||||
title="Explore Card"
|
||||
className='px-0'
|
||||
trigger={
|
||||
<div className="flex items-center space-x-1 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>Explore Card</span>
|
||||
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${activeExploreCardDropdown ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Other Navigation Items */}
|
||||
{navigationItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={`relative px-0 py-2 text-base font-medium transition-all duration-200 whitespace-nowrap group capitalize ${isNavItemActive(item.path)
|
||||
? 'text-primary'
|
||||
: 'text-gray-700 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
|
||||
@@ -387,8 +373,8 @@ export default function Navbar({
|
||||
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
|
||||
width: isNavItemActive(item.path) ? "100%" : 0,
|
||||
opacity: isNavItemActive(item.path) ? 1 : 0
|
||||
}}
|
||||
whileHover={{
|
||||
width: "100%",
|
||||
@@ -404,20 +390,12 @@ export default function Navbar({
|
||||
whileHover={{ scale: 1, opacity: 0.5 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</motion.button>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* 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">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Language Dropdown */}
|
||||
<Dropdown
|
||||
ref={languageRef}
|
||||
@@ -453,7 +431,7 @@ export default function Navbar({
|
||||
{
|
||||
id: 'checkout',
|
||||
label: 'Proceed to Checkout',
|
||||
action: onCheckoutClick
|
||||
path: '/checkout'
|
||||
}
|
||||
]}
|
||||
title="Shopping Cart"
|
||||
@@ -476,7 +454,7 @@ export default function Navbar({
|
||||
<div className="relative">
|
||||
<CTAButton
|
||||
user={user ?? null}
|
||||
onClick={user ? () => setActiveUserDropdown(prev => !prev) : (onSignInClick || (() => {}))}
|
||||
onClick={user ? () => setActiveUserDropdown(prev => !prev) : onSignInClick}
|
||||
className="hover:scale-105 transition-transform duration-200"
|
||||
/>
|
||||
|
||||
@@ -491,7 +469,7 @@ export default function Navbar({
|
||||
id: 'profile',
|
||||
label: 'My Profile',
|
||||
icon: <User className="w-4 h-4" />,
|
||||
action: onProfileClick
|
||||
path: '/profile'
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
@@ -519,11 +497,10 @@ export default function Navbar({
|
||||
|
||||
{/* 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={`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 */}
|
||||
@@ -531,13 +508,14 @@ export default function Navbar({
|
||||
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"
|
||||
/>
|
||||
<Link to="/">
|
||||
<ImageWithFallback
|
||||
src={logoImage}
|
||||
alt="CityCards Logo"
|
||||
className="h-10 w-auto"
|
||||
/>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* Mobile Actions */}
|
||||
@@ -547,7 +525,7 @@ export default function Navbar({
|
||||
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?.()}
|
||||
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">
|
||||
@@ -605,20 +583,19 @@ export default function Navbar({
|
||||
{/* 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' }
|
||||
{ label: 'How It Works', path: '/how-it-works' },
|
||||
{ label: 'Cities', path: '/melbourne' },
|
||||
{ label: 'Attractions', path: '/attractions' },
|
||||
{ label: 'Your Card', path: '/passes' },
|
||||
{ label: 'Deals', path: '/offers' }
|
||||
].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'
|
||||
}`}
|
||||
key={item.path}
|
||||
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.path)
|
||||
? '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 }}
|
||||
>
|
||||
@@ -627,15 +604,16 @@ export default function Navbar({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile Our Products Section */}
|
||||
{/* Mobile Explore Card Section */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 px-4">Our Products</h3>
|
||||
{productsItems.map((item) => (
|
||||
<h3 className="text-lg font-semibold text-gray-900 px-4">Explore Card</h3>
|
||||
{exploreCardItems.map((item) => (
|
||||
<motion.button
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
item.action?.();
|
||||
closeMobileMenu();
|
||||
if (item.path) {
|
||||
handleNavClick(item.path);
|
||||
}
|
||||
}}
|
||||
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 }}
|
||||
|
||||
@@ -9,30 +9,12 @@ import { CitySubmenu } from './CitySubmenu';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { Footer } from './Footer';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface PassesPageProps {
|
||||
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;
|
||||
onEsimsClick?: () => void;
|
||||
onHotelDiscountsClick?: () => void;
|
||||
onContactUsClick?: () => void;
|
||||
currentPage: string;
|
||||
user?: { email: string; name: string; } | null;
|
||||
}
|
||||
|
||||
@@ -162,27 +144,9 @@ const trustFeatures = [
|
||||
];
|
||||
|
||||
export function PassesPage({
|
||||
onHomeClick,
|
||||
onMelbourneClick,
|
||||
onPassesClick,
|
||||
onCheckoutClick,
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
onAttractionsClick,
|
||||
onBlogsClick,
|
||||
onHowItWorksClick,
|
||||
onFAQClick,
|
||||
onPrivacyPolicyClick,
|
||||
onAboutUsClick,
|
||||
onProfileClick,
|
||||
onCityCardsClick,
|
||||
onMagicItineraryClick,
|
||||
onPostCardsClick,
|
||||
onOffersClick,
|
||||
onEsimsClick,
|
||||
onHotelDiscountsClick,
|
||||
onContactUsClick,
|
||||
currentPage,
|
||||
user
|
||||
}: PassesPageProps) {
|
||||
const [selectedPass, setSelectedPass] = useState<string>('unlimited');
|
||||
@@ -199,44 +163,13 @@ export function PassesPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Navigation */}
|
||||
<Navbar
|
||||
activeCity="Melbourne"
|
||||
onCityChange={() => {}}
|
||||
onHomeClick={onHomeClick}
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
onPassesClick={onPassesClick}
|
||||
onCheckoutClick={onCheckoutClick}
|
||||
onAttractionsClick={onAttractionsClick}
|
||||
onBlogsClick={onBlogsClick}
|
||||
onHowItWorksClick={onHowItWorksClick}
|
||||
onFAQClick={onFAQClick}
|
||||
onPrivacyPolicyClick={onPrivacyPolicyClick}
|
||||
onAboutUsClick={onAboutUsClick}
|
||||
onProfileClick={onProfileClick}
|
||||
onCityCardsClick={onCityCardsClick}
|
||||
onMagicItineraryClick={onMagicItineraryClick}
|
||||
onPostCardsClick={onPostCardsClick}
|
||||
onOffersClick={onOffersClick}
|
||||
onEsimsClick={onEsimsClick}
|
||||
onHotelDiscountsClick={onHotelDiscountsClick}
|
||||
currentPage={currentPage}
|
||||
isUserSignedIn={!!user}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
<CitySubmenu
|
||||
currentPage={currentPage}
|
||||
onClose={() => {}}
|
||||
onHomeClick={onHomeClick}
|
||||
onMelbourneClick={onMelbourneClick}
|
||||
onAttractionsClick={onAttractionsClick}
|
||||
onPassesClick={onPassesClick}
|
||||
onBlogsClick={onBlogsClick}
|
||||
onHowItWorksClick={onHowItWorksClick}
|
||||
/>
|
||||
<Layout
|
||||
activeCity="Melbourne"
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
showCitySubmenu={true}
|
||||
>
|
||||
|
||||
<div className="container mx-auto px-4 pt-52 pb-12 relative z-10">
|
||||
{/* Page Header */}
|
||||
@@ -637,20 +570,7 @@ export function PassesPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer
|
||||
onHomeClick={onHomeClick}
|
||||
onMelbourneClick={onMelbourneClick}
|
||||
onPassesClick={onPassesClick}
|
||||
onSignInClick={onSignInClick}
|
||||
onAttractionsClick={onAttractionsClick}
|
||||
onBlogsClick={onBlogsClick}
|
||||
onHowItWorksClick={onHowItWorksClick}
|
||||
onFAQClick={onFAQClick}
|
||||
onPrivacyPolicyClick={onPrivacyPolicyClick}
|
||||
onContactUsClick={onContactUsClick}
|
||||
currentPage={currentPage}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
15
src/main.tsx
15
src/main.tsx
@@ -1,7 +1,10 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
);
|
||||
112
src/pages/ComingSoonPage.tsx
Normal file
112
src/pages/ComingSoonPage.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { Layout } from "../Layout";
|
||||
|
||||
interface ComingSoonPageProps {
|
||||
onSignInClick: () => void
|
||||
onSignOutClick?: () => void;
|
||||
user?: any; // Replace with your actual user type
|
||||
}
|
||||
|
||||
const ComingSoonPage: React.FC<ComingSoonPageProps> = ({
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
user,
|
||||
}) => {
|
||||
const scrollToNewsletter = () => {
|
||||
document.getElementById("newsletter-section")?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
const comingSoonBgImage = "https://images.unsplash.com/photo-1500760188161-40fc4f77594c?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjMyfHxwYXJpc3xlbnwwfHwwfHx8MA%3D%3D";
|
||||
|
||||
return (
|
||||
<Layout
|
||||
activeCity=""
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
showCitySubmenu={false}
|
||||
>
|
||||
<div
|
||||
className="relative z-10 min-h-screen flex items-end justify-start pt-24 pb-16 bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: `url(${comingSoonBgImage})` }}
|
||||
>
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/50 z-0"></div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<motion.div
|
||||
className="max-w-4xl lg:max-w-3xl xl:max-w-4xl"
|
||||
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],
|
||||
}}
|
||||
>
|
||||
{/* Coming Soon Badge */}
|
||||
<motion.div
|
||||
className="mb-6 inline-block"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.3, duration: 0.5 }}
|
||||
>
|
||||
<span className="bg-primary/20 text-primary font-poppins font-semibold px-4 py-2 rounded-full text-sm uppercase tracking-wide">
|
||||
Coming Soon
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Main Headline */}
|
||||
<div className="mb-6 text-left">
|
||||
<h1 className="font-poppins leading-tight text-white text-5xl md:text-6xl lg:text-7xl drop-shadow-lg">
|
||||
<span className="block font-normal">
|
||||
Something Amazing is Brewing
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Subheading */}
|
||||
<div className="mb-8 text-left">
|
||||
<p className="font-poppins text-xl leading-relaxed font-normal text-white/90 drop-shadow-md">
|
||||
We're working hard to bring you the ultimate city experience.
|
||||
<br className="hidden sm:block" />
|
||||
Be the first to know when we launch and get exclusive early
|
||||
access.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Countdown Timer */}
|
||||
<motion.div
|
||||
className="mb-8"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.9, duration: 0.6 }}
|
||||
>
|
||||
<div className="flex gap-4 justify-start">
|
||||
{[
|
||||
{ value: "14", label: "Days" },
|
||||
{ value: "08", label: "Hours" },
|
||||
].map((item, index) => (
|
||||
<div key={index} className="text-center">
|
||||
<div className="bg-white/20 backdrop-blur-sm rounded-lg p-3 min-w-[70px]">
|
||||
<span className="font-poppins font-bold text-2xl text-white">
|
||||
{item.value}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-poppins text-sm text-white/80 mt-2 block">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComingSoonPage;
|
||||
@@ -0,0 +1,216 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ChevronDown, MapPin, Star, Shield, Clock, Smartphone } from 'lucide-react';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { CitySubmenu } from '../components/CitySubmenu';
|
||||
import heroBannerImage from '../assets/landing-hero.png';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { LandingWhyChooseCityCards } from '../components/LandingWhyChooseCityCards';
|
||||
import { LandingVarietyOfAdventures } from '../components/LandingVarietyOfAdventures';
|
||||
import { LandingMagicItinerary } from '../components/LandingMagicItinerary';
|
||||
import { LandingBookAttractionSection } from '../components/LandingBookAttractionSection';
|
||||
import { LandingCustomPostcards } from '../components/LandingCustomPostcards';
|
||||
import { LandingUpcomingCities } from '../components/LandingUpcomingCities';
|
||||
import { LandingTrustSection } from '../components/LandingTrustSection';
|
||||
import { LandingMobileAppSection } from '../components/LandingMobileAppSection';
|
||||
import { LandingNewsletterSection } from '../components/LandingNewsletterSection';
|
||||
|
||||
|
||||
|
||||
const melbourneImage =
|
||||
"https://images.unsplash.com/photo-1551836022-d5d88e9218df?auto=format&fit=crop&w=1920&q=80"; // Melbourne
|
||||
|
||||
const sydneyImage =
|
||||
"https://images.unsplash.com/photo-1506976785307-8732e854ad03?auto=format&fit=crop&w=1920&q=80"; // Sydney Opera House
|
||||
|
||||
const brisbaneImage =
|
||||
"https://images.unsplash.com/photo-1604644363101-03f3d7cbecb6?auto=format&fit=crop&w=1920&q=80"; // Brisbane skyline
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface LandingPageProps {
|
||||
onSignInClick: () => void;
|
||||
onSignOutClick?: () => void;
|
||||
user?: User | null;
|
||||
}
|
||||
export function LandingPage({ onSignInClick,
|
||||
onSignOutClick,
|
||||
user }: LandingPageProps) {
|
||||
const [currentCityIndex, setCurrentCityIndex] = useState(0);
|
||||
|
||||
const cities = [
|
||||
{
|
||||
id: 'melbourne',
|
||||
name: 'Melbourne',
|
||||
description: 'Cultural capital with world-class attractions',
|
||||
image: melbourneImage,
|
||||
attractions: 45,
|
||||
savings: '30%',
|
||||
path: '/melbourne'
|
||||
},
|
||||
{
|
||||
id: 'sydney',
|
||||
name: 'Sydney',
|
||||
description: 'Iconic landmarks and harbor views',
|
||||
image: sydneyImage,
|
||||
attractions: 38,
|
||||
savings: '25%',
|
||||
path: '/sydney'
|
||||
},
|
||||
{
|
||||
id: 'brisbane',
|
||||
name: 'Brisbane',
|
||||
description: 'Sunshine, riverside dining, and adventure',
|
||||
image: brisbaneImage,
|
||||
attractions: 32,
|
||||
savings: '28%',
|
||||
path: '/brisbane'
|
||||
}
|
||||
];
|
||||
|
||||
// Auto-rotate cities
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentCityIndex((prev) => (prev + 1) % cities.length);
|
||||
}, 4000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const scrollToCities = () => {
|
||||
document.getElementById('cities-section')?.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Navbar */}
|
||||
<Navbar
|
||||
activeCity="Landingpage"
|
||||
onCityChange={(city) => {
|
||||
// Handle city change if needed, or remove this prop
|
||||
}}
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
isUserSignedIn={!!user}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
{/* City Submenu */}
|
||||
<CitySubmenu
|
||||
onClose={() => { }}
|
||||
/>
|
||||
{/* Hero Section */}
|
||||
<div
|
||||
className="relative z-10 min-h-screen flex items-end justify-start pt-24 pb-16 bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: `url(${heroBannerImage})` }}
|
||||
>
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/40 z-0"></div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<motion.div
|
||||
className="max-w-4xl lg:max-w-3xl xl:max-w-4xl"
|
||||
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],
|
||||
}}
|
||||
>
|
||||
{/* Main Headline */}
|
||||
<div className="mb-6 text-left">
|
||||
<h1 className="font-poppins leading-tight text-white text-5xl md:text-6xl lg:text-7xl drop-shadow-lg">
|
||||
<span className="block font-light">CityCards.</span>
|
||||
<span className="block font-normal">See More, Spend Less.</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Subheading */}
|
||||
<div className="mb-8 text-left">
|
||||
<p className="font-poppins text-xl leading-relaxed font-normal text-white/90 drop-shadow-md">
|
||||
Instant QR access to 40+ attractions,
|
||||
<br className="hidden sm:block" />
|
||||
exclusive perks, and savings up to 30%
|
||||
</p>
|
||||
</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],
|
||||
}}
|
||||
>
|
||||
<Link to="/melbourne">
|
||||
<Button
|
||||
withShine={true}
|
||||
size="xl"
|
||||
className="bg-primary hover:bg-primary/90 py-4 rounded-full text-lg font-poppins font-semibold px-8 text-white shadow-lg hover:shadow-xl transition-all duration-300"
|
||||
>
|
||||
Explore Cities
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Scroll Indicator */}
|
||||
<motion.div
|
||||
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 1.2, duration: 0.8 }}
|
||||
>
|
||||
<button
|
||||
onClick={scrollToCities}
|
||||
className="text-white hover:text-primary transition-colors duration-300"
|
||||
>
|
||||
<ChevronDown className="w-8 h-8 animate-bounce" />
|
||||
</button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<LandingWhyChooseCityCards />
|
||||
|
||||
{/* LandingVarietyOfAdventures Section */}
|
||||
<LandingVarietyOfAdventures />
|
||||
|
||||
{/* MagicItinerary Section */}
|
||||
<LandingMagicItinerary />
|
||||
|
||||
{/* BookAttractionSection Section */}
|
||||
<LandingBookAttractionSection />
|
||||
|
||||
{/* CustomPostcards Section */}
|
||||
<LandingCustomPostcards />
|
||||
|
||||
{/* UpcomingCities Section */}
|
||||
<LandingUpcomingCities />
|
||||
|
||||
{/* TrustSection Section */}
|
||||
<LandingTrustSection />
|
||||
|
||||
{/* MobileAppSection Section */}
|
||||
<LandingMobileAppSection />
|
||||
|
||||
{/* Newsletter Section */}
|
||||
<LandingNewsletterSection />
|
||||
|
||||
{/* Footer */}
|
||||
<Footer
|
||||
currentPage="melbourne"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user