Files
CityCards-Website/src/App.tsx

295 lines
11 KiB
TypeScript

import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { QrCode, X } from 'lucide-react';
import { useLocation, useNavigate } from 'react-router-dom';
import cityCardsLogo from './assets/cit-logo.png';
import imgRectangle18609 from "./assets/84c8cfb942f3c96e9f5fbb29459a9861138f61d8.png";
import { AppRouter } from './AppRouter';
import {
pageTransition,
easeOutExpo,
easeOutCubic
} from './utils/animations';
import { AuthProvider } from './context/AuthContext';
// User type definition
interface User {
email: string;
name: string;
}
function App() {
const [isMobile, setIsMobile] = useState(false);
const [showQRCard, setShowQRCard] = useState(false);
const [offersSource, setOffersSource] = useState<'products' | 'passes'>('products');
const [stickyCardType, setStickyCardType] = useState<'unlimited' | 'selective'>('unlimited');
// ✅ Authentication state management
const [user, setUser] = useState<User | null>(null);
const [showLoginModal, setShowLoginModal] = useState(false);
// ✅ City state management
const [activeCity, setActiveCity] = useState('');
const location = useLocation();
const navigate = useNavigate();
// ✅ Login handlers
const handleLoginSuccess = (userData: User) => {
setUser(userData);
setShowLoginModal(false);
console.log('User logged in successfully:', userData);
};
const handleSignOut = () => {
setUser(null);
navigate('/');
console.log('User signed out');
};
const handleCloseLoginModal = () => {
setShowLoginModal(false);
};
const handleSignInClick = () => {
setShowLoginModal(true);
};
// ✅ Handle city change
const handleCityChange = (city: string) => {
console.log('City changed to:', city);
setActiveCity(city);
};
// ✅ Handle checkout (you can expand this later)
const handleCheckoutClick = () => {
console.log('Proceeding to checkout for user:', user);
// Add your checkout logic here
navigate('/checkout');
};
// Detect mobile for optimized animations
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
// Generate a realistic QR code pattern
const generateQRPattern = () => {
const size = 27;
const pattern = [];
for (let i = 0; i < size * size; i++) {
const row = Math.floor(i / size);
const col = i % size;
const isCornerSquare =
(row < 7 && col < 7) ||
(row < 7 && col >= 20) ||
(row >= 20 && col < 7);
const isFinderPattern = isCornerSquare && (
(row === 0 || row === 6 || col === 0 || col === 6) ||
(row >= 2 && row <= 4 && col >= 2 && col <= 4)
);
const isTimingPattern = (row === 6 && col >= 8 && col <= 18) || (col === 6 && row >= 8 && row <= 18);
const isDataPattern = !isCornerSquare && !isTimingPattern && Math.random() > 0.38;
pattern.push(isFinderPattern || isTimingPattern || isDataPattern);
}
return pattern;
};
const qrPattern = generateQRPattern();
const handleStickyWidgetClick = () => {
if (location.pathname === '/attractions') {
navigate('/checkout');
} else {
setShowQRCard(true);
}
};
return (
<div className="min-h-screen bg-background relative">
{/* Global Animation Context Provider */}
<motion.div
className="relative z-10"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, ease: easeOutCubic }}
>
<AuthProvider>
<AppRouter
user={user}
activeCity={activeCity}
onCityChange={handleCityChange}
showLoginModal={showLoginModal}
onSignInClick={handleSignInClick}
onSignOutClick={handleSignOut}
onLoginSuccess={handleLoginSuccess}
onCloseLoginModal={handleCloseLoginModal}
onCheckoutClick={handleCheckoutClick} // ✅ Pass checkout handler
offersSource={offersSource}
/>
</AuthProvider>
</motion.div>
{/* Sticky Widget */}
<div className="fixed bottom-6 right-6 z-50">
<AnimatePresence>
{!showQRCard && (
<motion.button
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: easeOutExpo }}
whileHover={{ scale: 1.05, y: -2 }}
whileTap={{ scale: 0.95 }}
onClick={handleStickyWidgetClick}
className={`relative shadow-2xl flex items-center justify-center rounded-2xl transition-all duration-300 overflow-hidden group ${location.pathname === '/attractions'
? 'w-[244px] h-36'
: 'w-36 h-36 bg-black text-white'
}`}
aria-label={location.pathname === '/attractions' ? 'Get CityCard' : 'Show QR Code'}
>
{location.pathname === '/attractions' ? (
<div className="bg-black relative rounded-[12px] size-full">
<div className="overflow-clip relative size-full">
{/* Header Image */}
<div className="absolute h-[49px] left-0 rounded-tl-[12px] rounded-tr-[12px] top-0 w-full">
<div aria-hidden="true" className="absolute inset-0 pointer-events-none rounded-tl-[12px] rounded-tr-[12px]">
<img alt="" className="absolute max-w-none object-cover rounded-tl-[12px] rounded-tr-[12px] size-full" style={{ objectPosition: '50% 50%' }} src={imgRectangle18609} />
<div className="absolute bg-[rgba(0,0,0,0.42)] inset-0 rounded-tl-[12px] rounded-tr-[12px]" />
</div>
</div>
{/* GET NOW Text */}
<p className="absolute font-poppins font-semibold leading-[16px] left-[50%] -translate-x-1/2 not-italic text-[12px] text-nowrap text-white top-[17px] whitespace-pre">GET NOW</p>
{/* Dashed Line Separator */}
<div className="absolute h-0 left-0 top-[49px] w-full">
<div className="absolute bottom-0 left-0 right-0 top-[-1px]">
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 244 1">
<line stroke="#FFB23F" strokeDasharray="6 6" x2="244" y1="0.5" y2="0.5" />
</svg>
</div>
</div>
{/* Card Title in Orange */}
<p className="absolute font-appoppins font-medium leading-[1.3] left-[50%] text-[#ffb23f] text-[24px] text-center top-[65px] tracking-[-0.96px] translate-x-[-50%] w-[202px]" style={{ fontVariationSettings: "'wdth' 100" }}>
{stickyCardType === 'unlimited' ? (
<>Melbourne Unlimited Card</>
) : (
<>Get Selective Card</>
)}
</p>
</div>
{/* Orange Border */}
<div aria-hidden="true" className="absolute border-2 border-[#ffb23f] border-solid inset-0 pointer-events-none rounded-[12px]" />
</div>
) : (
<div className="relative flex flex-col items-center gap-2.5 z-10">
<QrCode className="w-14 h-14" />
<div className="text-center">
<div className="font-poppins font-semibold tracking-wide">
Scan QR
</div>
<div className="font-poppins text-xs opacity-70 leading-tight">
Download App
</div>
</div>
</div>
)}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-white/5 pointer-events-none rounded-2xl"></div>
</motion.button>
)}
</AnimatePresence>
<AnimatePresence>
{showQRCard && location.pathname !== '/attractions' && (
<motion.div
initial={{ scale: 0, opacity: 0, x: 20, y: 20 }}
animate={{ scale: 1, opacity: 1, x: 0, y: 0 }}
exit={{ scale: 0, opacity: 0, x: 20, y: 20 }}
transition={{ duration: 0.4, ease: easeOutExpo }}
className="bg-black p-14 shadow-2xl border border-gray-800 backdrop-blur-sm relative rounded-[5px]"
>
<button
onClick={() => setShowQRCard(false)}
className="absolute top-6 right-6 w-12 h-12 bg-gray-700 hover:bg-gray-600 flex items-center justify-center transition-colors duration-200 rounded-[5px]"
aria-label="Close QR Code"
>
<X className="w-6 h-6 text-white" />
</button>
<div className="w-120 h-120 bg-white p-10 mb-10 rounded-[5px]">
<div className="w-full h-full bg-white relative overflow-hidden rounded-[3px]">
<div className="grid grid-cols-27 gap-0 w-full h-full p-4">
{qrPattern.map((filled, index) => (
<motion.div
key={index}
className={`aspect-square ${filled ? 'bg-black' : 'bg-transparent'} rounded-[0.5px]`}
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.01,
delay: index * 0.001,
ease: "easeOut"
}}
/>
))}
</div>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-20 h-20 bg-white shadow-2xl border-4 border-gray-200 flex items-center justify-center rounded-[5px]">
<img
src={cityCardsLogo}
alt="CityCards"
className="w-16 h-16 object-contain"
/>
</div>
</div>
</div>
</div>
<div className="text-center text-white">
<h3 className="text-4xl font-semibold text-white mb-5">Scan QR for app</h3>
<p className="text-xl text-gray-300 leading-relaxed max-w-lg mx-auto">
Scan to get the Melbourne CityCards mobile app
</p>
</div>
<div className="absolute inset-0 border border-primary/20 pointer-events-none rounded-[5px]"></div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Preload animation optimization */}
<motion.div
className="fixed inset-0 bg-background z-50 pointer-events-none"
initial={{ opacity: 1 }}
animate={{ opacity: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
onAnimationComplete={() => {
const element = document.querySelector('[data-preload]');
element?.remove();
}}
data-preload
/>
</div>
);
}
export default App;