updates
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700;800;900&family=Cormorant+Garamond:wght@300;400;500;600;700&family=Cinzel:wght@400;500;600;700&family=Poppins:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
||||||
<title>Luxury Hotel - Excellence Redefined</title>
|
<title>Luxury Hotel - Excellence Redefined</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, lazy, Suspense } from 'react';
|
import { useEffect, lazy, Suspense } from 'react';
|
||||||
import {
|
import {
|
||||||
BrowserRouter,
|
BrowserRouter,
|
||||||
Routes,
|
Routes,
|
||||||
@@ -41,7 +41,7 @@ const SearchResultsPage = lazy(() => import('./pages/customer/SearchResultsPage'
|
|||||||
const FavoritesPage = lazy(() => import('./pages/customer/FavoritesPage'));
|
const FavoritesPage = lazy(() => import('./pages/customer/FavoritesPage'));
|
||||||
const MyBookingsPage = lazy(() => import('./pages/customer/MyBookingsPage'));
|
const MyBookingsPage = lazy(() => import('./pages/customer/MyBookingsPage'));
|
||||||
const BookingPage = lazy(() => import('./pages/customer/BookingPage'));
|
const BookingPage = lazy(() => import('./pages/customer/BookingPage'));
|
||||||
const BookingSuccessPage = lazy(() => import('./pages/customer/BookingSuccessPage'));
|
const BookingSuccessPage = lazy(() => import('./pages/customer/BookingSuccessPage') as Promise<{ default: React.ComponentType<any> }>);
|
||||||
const BookingDetailPage = lazy(() => import('./pages/customer/BookingDetailPage'));
|
const BookingDetailPage = lazy(() => import('./pages/customer/BookingDetailPage'));
|
||||||
const DepositPaymentPage = lazy(() => import('./pages/customer/DepositPaymentPage'));
|
const DepositPaymentPage = lazy(() => import('./pages/customer/DepositPaymentPage'));
|
||||||
const FullPaymentPage = lazy(() => import('./pages/customer/FullPaymentPage'));
|
const FullPaymentPage = lazy(() => import('./pages/customer/FullPaymentPage'));
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const ResetPasswordRouteHandler: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) {
|
if (token) {
|
||||||
openModal('reset-password', { token });
|
openModal('reset-password', { token });
|
||||||
// Navigate to home to keep user on current page
|
|
||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
}
|
}
|
||||||
}, [token, openModal, navigate]);
|
}, [token, openModal, navigate]);
|
||||||
|
|||||||
@@ -109,76 +109,85 @@ const Footer: React.FC = () => {
|
|||||||
: defaultSupportLinks;
|
: defaultSupportLinks;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="relative bg-gradient-to-b from-[#1a1a1a] via-[#0f0f0f] to-black text-gray-300 overflow-hidden">
|
<footer className="relative bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black text-gray-300 overflow-hidden">
|
||||||
{}
|
{/* Top Gold Accent Line */}
|
||||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-[#d4af37]/50 to-transparent"></div>
|
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-[#d4af37] to-transparent shadow-lg shadow-[#d4af37]/50"></div>
|
||||||
|
|
||||||
{}
|
{/* Decorative Pattern Overlay */}
|
||||||
<div className="absolute inset-0 opacity-5" style={{
|
<div className="absolute inset-0 opacity-[0.03]" 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%3Cpath d='M9 0h2v20H9zM25 0h2v20h-2zM41 0h2v20h-2zM57 0h2v20h-2zM0 9h20v2H0zM0 25h20v2H0zM0 41h20v2H0zM0 57h20v2H0zM40 9h20v2H40zM40 25h20v2H40zM40 41h20v2H40zM40 57h20v2H40zM9 40h2v20H9zM25 40h2v20h-2zM41 40h2v20h-2zM57 40h2v20h-2z' fill='%23d4af37' opacity='0.05'/%3E%3C/svg%3E")`
|
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 0h2v20H9zM25 0h2v20h-2zM41 0h2v20h-2zM57 0h2v20h-2zM0 9h20v2H0zM0 25h20v2H0zM0 41h20v2H0zM0 57h20v2H0zM40 9h20v2H40zM40 25h20v2H40zM40 41h20v2H40zM40 57h20v2H40zM9 40h2v20H9zM25 40h2v20h-2zM41 40h2v20h-2zM57 40h2v20h-2z' fill='%23d4af37'/%3E%3C/svg%3E")`
|
||||||
}}></div>
|
}}></div>
|
||||||
|
|
||||||
<div className="relative container mx-auto px-6 lg:px-8 py-16 lg:py-20">
|
{/* Subtle Gradient Overlay */}
|
||||||
{}
|
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent pointer-events-none"></div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-12 lg:gap-16 mb-16">
|
|
||||||
{}
|
<div className="relative container mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
|
||||||
|
{/* Main Content Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-10 sm:gap-12 lg:gap-16 mb-16 sm:mb-20">
|
||||||
|
{/* Brand Section */}
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<div className="flex items-center space-x-3 mb-6">
|
<div className="flex items-center space-x-4 mb-6 sm:mb-8">
|
||||||
{logoUrl ? (
|
{logoUrl ? (
|
||||||
<div className="relative">
|
<div className="relative group">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-[#d4af37]/20 to-transparent rounded-lg blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
<img
|
<img
|
||||||
src={logoUrl}
|
src={logoUrl}
|
||||||
alt={settings.company_name}
|
alt={settings.company_name}
|
||||||
className="h-10 w-auto object-contain drop-shadow-lg"
|
className="h-12 sm:h-14 w-auto object-contain drop-shadow-2xl relative z-10 filter brightness-110"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative">
|
<div className="relative group">
|
||||||
<Hotel className="w-10 h-10 text-[#d4af37]" />
|
<div className="p-3 bg-gradient-to-br from-[#d4af37]/10 to-[#c9a227]/5 rounded-lg border border-[#d4af37]/20 backdrop-blur-sm">
|
||||||
<div className="absolute inset-0 bg-[#d4af37]/20 blur-xl"></div>
|
<Hotel className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37] relative z-10 drop-shadow-lg" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 bg-[#d4af37]/20 blur-2xl opacity-50 group-hover:opacity-75 transition-opacity duration-500"></div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<span className="text-2xl font-serif font-semibold text-white tracking-wide">
|
<h2 className="text-2xl sm:text-3xl font-display font-semibold text-white tracking-tight mb-1">
|
||||||
{settings.company_name || pageContent?.title || 'Luxury Hotel'}
|
{settings.company_name || pageContent?.title || 'Luxury Hotel'}
|
||||||
</span>
|
</h2>
|
||||||
<p className="text-xs text-[#d4af37]/80 font-light tracking-widest uppercase mt-0.5">
|
<p className="text-xs sm:text-sm text-[#d4af37] font-light tracking-[3px] sm:tracking-[4px] uppercase">
|
||||||
{settings.company_tagline || 'Excellence Redefined'}
|
{settings.company_tagline || 'Excellence Redefined'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-400 mb-6 leading-relaxed max-w-md">
|
<p className="text-sm sm:text-base text-gray-400 mb-8 leading-relaxed max-w-md font-light">
|
||||||
{pageContent?.description || 'Experience unparalleled luxury and world-class hospitality. Your journey to exceptional comfort begins here.'}
|
{pageContent?.description || 'Experience unparalleled luxury and world-class hospitality. Your journey to exceptional comfort begins here.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{}
|
{/* Badges */}
|
||||||
{badges.length > 0 && badges.some(b => b.text) && (
|
{badges.length > 0 && badges.some(b => b.text) && (
|
||||||
<div className="flex items-center space-x-6 mb-8">
|
<div className="flex flex-wrap items-center gap-4 sm:gap-6 mb-8">
|
||||||
{badges.map((badge, index) => {
|
{badges.map((badge, index) => {
|
||||||
if (!badge.text) return null;
|
if (!badge.text) return null;
|
||||||
const BadgeIcon = iconMap[badge.icon] || Award;
|
const BadgeIcon = iconMap[badge.icon] || Award;
|
||||||
return (
|
return (
|
||||||
<div key={index} className="flex items-center space-x-2 text-[#d4af37]/90">
|
<div
|
||||||
<BadgeIcon className="w-5 h-5" />
|
key={index}
|
||||||
<span className="text-xs font-medium tracking-wide">{badge.text}</span>
|
className="group flex items-center space-x-2 px-3 py-2 bg-gradient-to-r from-[#d4af37]/5 to-transparent border border-[#d4af37]/10 rounded-lg hover:border-[#d4af37]/30 hover:from-[#d4af37]/10 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<BadgeIcon className="w-4 h-4 sm:w-5 sm:h-5 text-[#d4af37] group-hover:scale-110 transition-transform duration-300" />
|
||||||
|
<span className="text-xs sm:text-sm font-medium tracking-wide text-gray-300 group-hover:text-[#d4af37] transition-colors">{badge.text}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{}
|
{/* Social Media Links */}
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3 sm:space-x-4">
|
||||||
{pageContent?.social_links?.facebook && (
|
{pageContent?.social_links?.facebook && (
|
||||||
<a
|
<a
|
||||||
href={pageContent.social_links.facebook}
|
href={pageContent.social_links.facebook}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative w-11 h-11 flex items-center justify-center rounded-full bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 hover:border-[#d4af37]/50 transition-all duration-300 hover:bg-[#d4af37]/10"
|
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[#d4af37]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[#d4af37]/10 hover:to-[#c9a227]/10 hover:shadow-lg hover:shadow-[#d4af37]/20 hover:-translate-y-0.5"
|
||||||
aria-label="Facebook"
|
aria-label="Facebook"
|
||||||
>
|
>
|
||||||
<Facebook className="w-5 h-5 text-gray-400 group-hover:text-[#d4af37] transition-colors" />
|
<Facebook className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[#d4af37] transition-all duration-300 group-hover:scale-110" />
|
||||||
<div className="absolute inset-0 rounded-full bg-[#d4af37]/0 group-hover:bg-[#d4af37]/20 blur-lg transition-all duration-300"></div>
|
<div className="absolute inset-0 rounded-lg bg-[#d4af37]/0 group-hover:bg-[#d4af37]/10 blur-xl transition-all duration-500"></div>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{pageContent?.social_links?.twitter && (
|
{pageContent?.social_links?.twitter && (
|
||||||
@@ -186,11 +195,11 @@ const Footer: React.FC = () => {
|
|||||||
href={pageContent.social_links.twitter}
|
href={pageContent.social_links.twitter}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative w-11 h-11 flex items-center justify-center rounded-full bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 hover:border-[#d4af37]/50 transition-all duration-300 hover:bg-[#d4af37]/10"
|
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[#d4af37]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[#d4af37]/10 hover:to-[#c9a227]/10 hover:shadow-lg hover:shadow-[#d4af37]/20 hover:-translate-y-0.5"
|
||||||
aria-label="Twitter"
|
aria-label="Twitter"
|
||||||
>
|
>
|
||||||
<Twitter className="w-5 h-5 text-gray-400 group-hover:text-[#d4af37] transition-colors" />
|
<Twitter className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[#d4af37] transition-all duration-300 group-hover:scale-110" />
|
||||||
<div className="absolute inset-0 rounded-full bg-[#d4af37]/0 group-hover:bg-[#d4af37]/20 blur-lg transition-all duration-300"></div>
|
<div className="absolute inset-0 rounded-lg bg-[#d4af37]/0 group-hover:bg-[#d4af37]/10 blur-xl transition-all duration-500"></div>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{pageContent?.social_links?.instagram && (
|
{pageContent?.social_links?.instagram && (
|
||||||
@@ -198,11 +207,11 @@ const Footer: React.FC = () => {
|
|||||||
href={pageContent.social_links.instagram}
|
href={pageContent.social_links.instagram}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative w-11 h-11 flex items-center justify-center rounded-full bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 hover:border-[#d4af37]/50 transition-all duration-300 hover:bg-[#d4af37]/10"
|
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[#d4af37]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[#d4af37]/10 hover:to-[#c9a227]/10 hover:shadow-lg hover:shadow-[#d4af37]/20 hover:-translate-y-0.5"
|
||||||
aria-label="Instagram"
|
aria-label="Instagram"
|
||||||
>
|
>
|
||||||
<Instagram className="w-5 h-5 text-gray-400 group-hover:text-[#d4af37] transition-colors" />
|
<Instagram className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[#d4af37] transition-all duration-300 group-hover:scale-110" />
|
||||||
<div className="absolute inset-0 rounded-full bg-[#d4af37]/0 group-hover:bg-[#d4af37]/20 blur-lg transition-all duration-300"></div>
|
<div className="absolute inset-0 rounded-lg bg-[#d4af37]/0 group-hover:bg-[#d4af37]/10 blur-xl transition-all duration-500"></div>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{pageContent?.social_links?.linkedin && (
|
{pageContent?.social_links?.linkedin && (
|
||||||
@@ -210,11 +219,11 @@ const Footer: React.FC = () => {
|
|||||||
href={pageContent.social_links.linkedin}
|
href={pageContent.social_links.linkedin}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative w-11 h-11 flex items-center justify-center rounded-full bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 hover:border-[#d4af37]/50 transition-all duration-300 hover:bg-[#d4af37]/10"
|
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[#d4af37]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[#d4af37]/10 hover:to-[#c9a227]/10 hover:shadow-lg hover:shadow-[#d4af37]/20 hover:-translate-y-0.5"
|
||||||
aria-label="LinkedIn"
|
aria-label="LinkedIn"
|
||||||
>
|
>
|
||||||
<Linkedin className="w-5 h-5 text-gray-400 group-hover:text-[#d4af37] transition-colors" />
|
<Linkedin className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[#d4af37] transition-all duration-300 group-hover:scale-110" />
|
||||||
<div className="absolute inset-0 rounded-full bg-[#d4af37]/0 group-hover:bg-[#d4af37]/20 blur-lg transition-all duration-300"></div>
|
<div className="absolute inset-0 rounded-lg bg-[#d4af37]/0 group-hover:bg-[#d4af37]/10 blur-xl transition-all duration-500"></div>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{pageContent?.social_links?.youtube && (
|
{pageContent?.social_links?.youtube && (
|
||||||
@@ -222,71 +231,73 @@ const Footer: React.FC = () => {
|
|||||||
href={pageContent.social_links.youtube}
|
href={pageContent.social_links.youtube}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative w-11 h-11 flex items-center justify-center rounded-full bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 hover:border-[#d4af37]/50 transition-all duration-300 hover:bg-[#d4af37]/10"
|
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[#d4af37]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[#d4af37]/10 hover:to-[#c9a227]/10 hover:shadow-lg hover:shadow-[#d4af37]/20 hover:-translate-y-0.5"
|
||||||
aria-label="YouTube"
|
aria-label="YouTube"
|
||||||
>
|
>
|
||||||
<Youtube className="w-5 h-5 text-gray-400 group-hover:text-[#d4af37] transition-colors" />
|
<Youtube className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[#d4af37] transition-all duration-300 group-hover:scale-110" />
|
||||||
<div className="absolute inset-0 rounded-full bg-[#d4af37]/0 group-hover:bg-[#d4af37]/20 blur-lg transition-all duration-300"></div>
|
<div className="absolute inset-0 rounded-lg bg-[#d4af37]/0 group-hover:bg-[#d4af37]/10 blur-xl transition-all duration-500"></div>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
{/* Quick Links */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-white font-semibold text-lg mb-6 relative inline-block tracking-wide">
|
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-6 sm:mb-8 relative inline-block tracking-wide">
|
||||||
<span className="relative z-10">Quick Links</span>
|
<span className="relative z-10">Quick Links</span>
|
||||||
<span className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-[#d4af37]/50 to-transparent"></span>
|
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[#d4af37] via-[#d4af37]/50 to-transparent"></span>
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3 sm:space-y-4">
|
||||||
{quickLinks.map((link) => (
|
{quickLinks.map((link) => (
|
||||||
<li key={link.url}>
|
<li key={link.url}>
|
||||||
<Link
|
<Link
|
||||||
to={link.url}
|
to={link.url}
|
||||||
className="group flex items-center text-sm text-gray-400 hover:text-[#d4af37] transition-all duration-300 relative font-light tracking-wide"
|
className="group flex items-center text-sm sm:text-base text-gray-400 hover:text-[#d4af37] transition-all duration-300 relative font-light tracking-wide"
|
||||||
>
|
>
|
||||||
<span className="absolute left-0 w-0 h-px bg-[#d4af37] group-hover:w-6 transition-all duration-300"></span>
|
<span className="absolute left-0 w-0 h-[2px] bg-gradient-to-r from-[#d4af37] to-[#c9a227] group-hover:w-8 transition-all duration-300 rounded-full"></span>
|
||||||
<span className="ml-8 group-hover:translate-x-1 transition-transform duration-300">{link.label}</span>
|
<span className="ml-10 group-hover:translate-x-2 transition-transform duration-300 group-hover:font-medium">{link.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
{/* Guest Services */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-white font-semibold text-lg mb-6 relative inline-block tracking-wide">
|
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-6 sm:mb-8 relative inline-block tracking-wide">
|
||||||
<span className="relative z-10">Guest Services</span>
|
<span className="relative z-10">Guest Services</span>
|
||||||
<span className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-[#d4af37]/50 to-transparent"></span>
|
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[#d4af37] via-[#d4af37]/50 to-transparent"></span>
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3 sm:space-y-4">
|
||||||
{supportLinks.map((link) => (
|
{supportLinks.map((link) => (
|
||||||
<li key={link.url}>
|
<li key={link.url}>
|
||||||
<Link
|
<Link
|
||||||
to={link.url}
|
to={link.url}
|
||||||
className="group flex items-center text-sm text-gray-400 hover:text-[#d4af37] transition-all duration-300 relative font-light tracking-wide"
|
className="group flex items-center text-sm sm:text-base text-gray-400 hover:text-[#d4af37] transition-all duration-300 relative font-light tracking-wide"
|
||||||
>
|
>
|
||||||
<span className="absolute left-0 w-0 h-px bg-[#d4af37] group-hover:w-6 transition-all duration-300"></span>
|
<span className="absolute left-0 w-0 h-[2px] bg-gradient-to-r from-[#d4af37] to-[#c9a227] group-hover:w-8 transition-all duration-300 rounded-full"></span>
|
||||||
<span className="ml-8 group-hover:translate-x-1 transition-transform duration-300">{link.label}</span>
|
<span className="ml-10 group-hover:translate-x-2 transition-transform duration-300 group-hover:font-medium">{link.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
{/* Contact Information */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-white font-semibold text-lg mb-6 relative inline-block tracking-wide">
|
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-6 sm:mb-8 relative inline-block tracking-wide">
|
||||||
<span className="relative z-10">Contact</span>
|
<span className="relative z-10">Contact</span>
|
||||||
<span className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-[#d4af37]/50 to-transparent"></span>
|
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[#d4af37] via-[#d4af37]/50 to-transparent"></span>
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-4">
|
<ul className="space-y-5 sm:space-y-6">
|
||||||
<li className="flex items-start space-x-4 group">
|
<li className="flex items-start space-x-4 group">
|
||||||
<div className="relative mt-0.5">
|
<div className="relative mt-1 flex-shrink-0">
|
||||||
<MapPin className="w-5 h-5 text-[#d4af37]/80 group-hover:text-[#d4af37] transition-colors flex-shrink-0" />
|
<div className="p-2 bg-gradient-to-br from-[#d4af37]/10 to-[#c9a227]/5 rounded-lg border border-[#d4af37]/20 group-hover:border-[#d4af37]/40 transition-all duration-300">
|
||||||
<div className="absolute inset-0 bg-[#d4af37]/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
<MapPin className="w-4 h-4 sm:w-5 sm:h-5 text-[#d4af37] transition-all duration-300 group-hover:scale-110" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 bg-[#d4af37]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-gray-400 group-hover:text-gray-300 transition-colors leading-relaxed font-light">
|
<span className="text-sm sm:text-base text-gray-400 group-hover:text-gray-300 transition-colors leading-relaxed font-light pt-1">
|
||||||
{(displayAddress
|
{(displayAddress
|
||||||
.split('\n').map((line, i) => (
|
.split('\n').map((line, i) => (
|
||||||
<React.Fragment key={i}>
|
<React.Fragment key={i}>
|
||||||
@@ -298,55 +309,59 @@ const Footer: React.FC = () => {
|
|||||||
</li>
|
</li>
|
||||||
{displayPhone && (
|
{displayPhone && (
|
||||||
<li className="flex items-center space-x-4 group">
|
<li className="flex items-center space-x-4 group">
|
||||||
<div className="relative">
|
<div className="relative flex-shrink-0">
|
||||||
<Phone className="w-5 h-5 text-[#d4af37]/80 group-hover:text-[#d4af37] transition-colors flex-shrink-0" />
|
<div className="p-2 bg-gradient-to-br from-[#d4af37]/10 to-[#c9a227]/5 rounded-lg border border-[#d4af37]/20 group-hover:border-[#d4af37]/40 transition-all duration-300">
|
||||||
<div className="absolute inset-0 bg-[#d4af37]/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
<Phone className="w-4 h-4 sm:w-5 sm:h-5 text-[#d4af37] transition-all duration-300 group-hover:scale-110" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 bg-[#d4af37]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<a href={phoneHref} className="text-sm text-gray-400 group-hover:text-[#d4af37] transition-colors font-light tracking-wide">
|
<a href={phoneHref} className="text-sm sm:text-base text-gray-400 group-hover:text-[#d4af37] transition-colors font-light tracking-wide">
|
||||||
{displayPhone}
|
{displayPhone}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{displayEmail && (
|
{displayEmail && (
|
||||||
<li className="flex items-center space-x-4 group">
|
<li className="flex items-center space-x-4 group">
|
||||||
<div className="relative">
|
<div className="relative flex-shrink-0">
|
||||||
<Mail className="w-5 h-5 text-[#d4af37]/80 group-hover:text-[#d4af37] transition-colors flex-shrink-0" />
|
<div className="p-2 bg-gradient-to-br from-[#d4af37]/10 to-[#c9a227]/5 rounded-lg border border-[#d4af37]/20 group-hover:border-[#d4af37]/40 transition-all duration-300">
|
||||||
<div className="absolute inset-0 bg-[#d4af37]/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
<Mail className="w-4 h-4 sm:w-5 sm:h-5 text-[#d4af37] transition-all duration-300 group-hover:scale-110" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 bg-[#d4af37]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<a href={`mailto:${displayEmail}`} className="text-sm text-gray-400 group-hover:text-[#d4af37] transition-colors font-light tracking-wide">
|
<a href={`mailto:${displayEmail}`} className="text-sm sm:text-base text-gray-400 group-hover:text-[#d4af37] transition-colors font-light tracking-wide break-all">
|
||||||
{displayEmail}
|
{displayEmail}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{}
|
{/* Rating Section */}
|
||||||
<div className="mt-6 pt-6 border-t border-gray-800/50">
|
<div className="mt-8 sm:mt-10 pt-6 sm:pt-8 border-t border-gray-800/50">
|
||||||
<div className="flex items-center space-x-1 mb-2">
|
<div className="flex items-center space-x-1 mb-3">
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<Star key={i} className="w-4 h-4 fill-[#d4af37] text-[#d4af37]" />
|
<Star key={i} className="w-4 h-4 sm:w-5 sm:h-5 fill-[#d4af37] text-[#d4af37] drop-shadow-lg" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 font-light">Rated 5.0 by 10,000+ guests</p>
|
<p className="text-xs sm:text-sm text-gray-500 font-light tracking-wide">Rated 5.0 by 10,000+ guests</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
{/* Divider */}
|
||||||
<div className="relative my-12">
|
<div className="relative my-12 sm:my-16">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<div className="absolute inset-0 flex items-center">
|
||||||
<div className="w-full border-t border-gray-800"></div>
|
<div className="w-full border-t border-gray-800/50"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center">
|
<div className="relative flex justify-center">
|
||||||
<div className="bg-gray-900 px-4">
|
<div className="bg-gradient-to-b from-[#0f0f0f] to-[#1a1a1a] px-6">
|
||||||
<div className="w-16 h-px bg-gradient-to-r from-transparent via-[#d4af37]/30 to-transparent"></div>
|
<div className="w-24 sm:w-32 h-[2px] bg-gradient-to-r from-transparent via-[#d4af37]/50 to-transparent shadow-lg shadow-[#d4af37]/30"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
{/* Bottom Section */}
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
|
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
|
||||||
<div className="text-sm text-gray-500 font-light tracking-wide">
|
<div className="text-sm sm:text-base text-gray-500 font-light tracking-wide text-center md:text-left">
|
||||||
{(() => {
|
{(() => {
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const copyrightText = pageContent?.copyright_text || '© {YEAR} Luxury Hotel. All rights reserved.';
|
const copyrightText = pageContent?.copyright_text || '© {YEAR} Luxury Hotel. All rights reserved.';
|
||||||
@@ -354,20 +369,20 @@ const Footer: React.FC = () => {
|
|||||||
return copyrightText.replace(/{YEAR}/g, currentYear.toString());
|
return copyrightText.replace(/{YEAR}/g, currentYear.toString());
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-6 text-xs text-gray-600">
|
<div className="flex items-center space-x-4 sm:space-x-6 text-xs sm:text-sm text-gray-600">
|
||||||
<span className="hover:text-[#d4af37]/80 transition-colors cursor-pointer font-light tracking-wide">Privacy</span>
|
<span className="hover:text-[#d4af37] transition-colors cursor-pointer font-light tracking-wide">Privacy</span>
|
||||||
<span className="text-gray-700">•</span>
|
<span className="text-gray-700">•</span>
|
||||||
<span className="hover:text-[#d4af37]/80 transition-colors cursor-pointer font-light tracking-wide">Terms</span>
|
<span className="hover:text-[#d4af37] transition-colors cursor-pointer font-light tracking-wide">Terms</span>
|
||||||
<span className="text-gray-700">•</span>
|
<span className="text-gray-700">•</span>
|
||||||
<CookiePreferencesLink />
|
<CookiePreferencesLink />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
{/* Bottom Gold Accent Line */}
|
||||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-[#d4af37]/30 to-transparent"></div>
|
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-[#d4af37] to-transparent shadow-lg shadow-[#d4af37]/50"></div>
|
||||||
|
|
||||||
{}
|
{/* Chat Widget */}
|
||||||
<ChatWidget />
|
<ChatWidget />
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-2xl font-serif font-semibold text-white tracking-tight leading-tight">
|
<span className="text-2xl font-display font-semibold text-white tracking-tight leading-tight">
|
||||||
{settings.company_name}
|
{settings.company_name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-[#d4af37] tracking-[0.2em] uppercase font-light">
|
<span className="text-[10px] text-[#d4af37] tracking-[0.2em] uppercase font-light">
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
import LoginModal from './LoginModal';
|
import LoginModal from './LoginModal';
|
||||||
import RegisterModal from './RegisterModal';
|
import RegisterModal from './RegisterModal';
|
||||||
import ForgotPasswordModal from './ForgotPasswordModal';
|
import ForgotPasswordModal from './ForgotPasswordModal';
|
||||||
import ResetPasswordModal from './ResetPasswordModal';
|
import ResetPasswordModal from './ResetPasswordModal';
|
||||||
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
|
||||||
const AuthModalManager: React.FC = () => {
|
const AuthModalManager: React.FC = () => {
|
||||||
const { isOpen, modalType, resetPasswordParams } = useAuthModal();
|
const { isOpen, modalType, resetPasswordParams, openModal } = useAuthModal();
|
||||||
|
const { isAuthenticated } = useAuthStore();
|
||||||
|
|
||||||
|
// Listen for auth:logout event from apiClient
|
||||||
|
useEffect(() => {
|
||||||
|
const handleAuthLogout = (event: CustomEvent) => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
openModal('login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('auth:logout', handleAuthLogout as EventListener);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('auth:logout', handleAuthLogout as EventListener);
|
||||||
|
};
|
||||||
|
}, [openModal, isAuthenticated]);
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const ForgotPasswordModal: React.FC = () => {
|
|||||||
{settings.company_tagline}
|
{settings.company_tagline}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<h2 className="text-xl sm:text-2xl lg:text-3xl font-serif font-semibold text-gray-900 tracking-tight">
|
<h2 className="text-xl sm:text-2xl lg:text-3xl font-elegant font-semibold text-gray-900 tracking-tight">
|
||||||
Forgot Password?
|
Forgot Password?
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ const LoginModal: React.FC = () => {
|
|||||||
{settings.company_tagline}
|
{settings.company_tagline}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<h2 className="text-xl sm:text-2xl lg:text-3xl font-serif font-semibold text-gray-900 tracking-tight">
|
<h2 className="text-xl sm:text-2xl lg:text-3xl font-elegant font-semibold text-gray-900 tracking-tight">
|
||||||
{requiresMFA ? 'Verify Your Identity' : 'Welcome Back'}
|
{requiresMFA ? 'Verify Your Identity' : 'Welcome Back'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ const RegisterModal: React.FC = () => {
|
|||||||
{settings.company_tagline}
|
{settings.company_tagline}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<h2 className="text-xl sm:text-2xl lg:text-3xl font-serif font-semibold text-gray-900 tracking-tight">
|
<h2 className="text-xl sm:text-2xl lg:text-3xl font-elegant font-semibold text-gray-900 tracking-tight">
|
||||||
Create Account
|
Create Account
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ const ResetPasswordModal: React.FC<ResetPasswordModalProps> = ({ token }) => {
|
|||||||
{settings.company_tagline}
|
{settings.company_tagline}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<h2 className="text-xl sm:text-2xl lg:text-3xl font-serif font-semibold text-gray-900 tracking-tight">
|
<h2 className="text-xl sm:text-2xl lg:text-3xl font-elegant font-semibold text-gray-900 tracking-tight">
|
||||||
{isSuccess ? 'Complete!' : 'Reset Password'}
|
{isSuccess ? 'Complete!' : 'Reset Password'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
type Review,
|
type Review,
|
||||||
} from '../../services/api/reviewService';
|
} from '../../services/api/reviewService';
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
import Recaptcha from '../common/Recaptcha';
|
import Recaptcha from '../common/Recaptcha';
|
||||||
import { recaptchaService } from '../../services/api/systemSettingsService';
|
import { recaptchaService } from '../../services/api/systemSettingsService';
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
roomId
|
roomId
|
||||||
}) => {
|
}) => {
|
||||||
const { isAuthenticated } = useAuthStore();
|
const { isAuthenticated } = useAuthStore();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
const [reviews, setReviews] = useState<Review[]>([]);
|
const [reviews, setReviews] = useState<Review[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@@ -257,13 +259,13 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
>
|
>
|
||||||
<p className="text-[#d4af37] text-xs sm:text-sm font-light">
|
<p className="text-[#d4af37] text-xs sm:text-sm font-light">
|
||||||
Please{' '}
|
Please{' '}
|
||||||
<a
|
<button
|
||||||
href="/login"
|
onClick={() => openModal('login')}
|
||||||
className="font-semibold underline
|
className="font-semibold underline
|
||||||
hover:text-[#f5d76e] transition-colors"
|
hover:text-[#f5d76e] transition-colors"
|
||||||
>
|
>
|
||||||
login
|
login
|
||||||
</a>{' '}
|
</button>{' '}
|
||||||
to write a review
|
to write a review
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const NotFoundPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* 404 Number */}
|
{/* 404 Number */}
|
||||||
<div className="mb-6 sm:mb-8">
|
<div className="mb-6 sm:mb-8">
|
||||||
<h1 className="text-8xl sm:text-9xl lg:text-[12rem] font-serif font-bold text-transparent bg-clip-text bg-gradient-to-r from-[#d4af37] via-[#f5d76e] to-[#d4af37] leading-none tracking-tight">
|
<h1 className="text-8xl sm:text-9xl lg:text-[12rem] font-display font-bold text-transparent bg-clip-text bg-gradient-to-r from-[#d4af37] via-[#f5d76e] to-[#d4af37] leading-none tracking-tight">
|
||||||
404
|
404
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,7 +59,7 @@ const NotFoundPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* Main Message */}
|
{/* Main Message */}
|
||||||
<div className="mb-6 sm:mb-8">
|
<div className="mb-6 sm:mb-8">
|
||||||
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-serif font-semibold text-gray-900 mb-3 sm:mb-4 tracking-tight">
|
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-elegant font-semibold text-gray-900 mb-3 sm:mb-4 tracking-tight">
|
||||||
Page Not Found
|
Page Not Found
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-base sm:text-lg text-gray-600 font-light tracking-wide max-w-md mx-auto leading-relaxed">
|
<p className="text-base sm:text-lg text-gray-600 font-light tracking-wide max-w-md mx-auto leading-relaxed">
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export { default as LoginPage } from './LoginPage';
|
|
||||||
export { default as RegisterPage } from './RegisterPage';
|
|
||||||
export { default as ForgotPasswordPage } from './ForgotPasswordPage';
|
|
||||||
export { default as ResetPasswordPage } from './ResetPasswordPage';
|
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
type Booking,
|
type Booking,
|
||||||
} from '../../services/api/bookingService';
|
} from '../../services/api/bookingService';
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
import Loading from '../../components/common/Loading';
|
import Loading from '../../components/common/Loading';
|
||||||
import PaymentStatusBadge from
|
import PaymentStatusBadge from
|
||||||
'../../components/common/PaymentStatusBadge';
|
'../../components/common/PaymentStatusBadge';
|
||||||
@@ -40,6 +41,7 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAuthenticated } = useAuthStore();
|
const { isAuthenticated } = useAuthStore();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
const { formatCurrency } = useFormatCurrency();
|
const { formatCurrency } = useFormatCurrency();
|
||||||
|
|
||||||
const [booking, setBooking] = useState<Booking | null>(
|
const [booking, setBooking] = useState<Booking | null>(
|
||||||
@@ -55,11 +57,9 @@ const BookingDetailPage: React.FC = () => {
|
|||||||
toast.error(
|
toast.error(
|
||||||
'Please login to view booking details'
|
'Please login to view booking details'
|
||||||
);
|
);
|
||||||
navigate('/login', {
|
openModal('login');
|
||||||
state: { from: `/bookings/${id}` }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, navigate, id]);
|
}, [isAuthenticated, openModal, id]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
import { serviceService, Service, promotionService, Promotion } from '../../services/api';
|
import { serviceService, Service, promotionService, Promotion } from '../../services/api';
|
||||||
import { createPayPalOrder } from '../../services/api/paymentService';
|
import { createPayPalOrder } from '../../services/api/paymentService';
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
import {
|
import {
|
||||||
bookingValidationSchema,
|
bookingValidationSchema,
|
||||||
type BookingFormData
|
type BookingFormData
|
||||||
@@ -49,6 +50,7 @@ const BookingPage: React.FC = () => {
|
|||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAuthenticated, userInfo } = useAuthStore();
|
const { isAuthenticated, userInfo } = useAuthStore();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
const { formatCurrency, currency } = useFormatCurrency();
|
const { formatCurrency, currency } = useFormatCurrency();
|
||||||
|
|
||||||
const [room, setRoom] = useState<Room | null>(null);
|
const [room, setRoom] = useState<Room | null>(null);
|
||||||
@@ -71,9 +73,7 @@ const BookingPage: React.FC = () => {
|
|||||||
toast.error(
|
toast.error(
|
||||||
'Please login to make a booking'
|
'Please login to make a booking'
|
||||||
);
|
);
|
||||||
navigate('/login', {
|
openModal('login');
|
||||||
state: { from: `/booking/${id}` }
|
|
||||||
});
|
|
||||||
} else if (userInfo?.role === 'admin' || userInfo?.role === 'staff') {
|
} else if (userInfo?.role === 'admin' || userInfo?.role === 'staff') {
|
||||||
toast.error('Admin and staff users cannot make bookings');
|
toast.error('Admin and staff users cannot make bookings');
|
||||||
if (userInfo?.role === 'admin') {
|
if (userInfo?.role === 'admin') {
|
||||||
@@ -1284,17 +1284,13 @@ const BookingPage: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-400 mb-2 font-light tracking-wide">
|
<p className="text-xs text-gray-400 mb-2 font-light tracking-wide">
|
||||||
Pay the remaining balance on arrival
|
Requires 20% deposit, remaining balance on arrival
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-gradient-to-br from-orange-900/20 to-orange-800/10
|
<div className="bg-gradient-to-br from-orange-900/20 to-orange-800/10
|
||||||
border border-orange-500/30 rounded-lg p-2.5"
|
border border-orange-500/30 rounded-lg p-2.5"
|
||||||
>
|
>
|
||||||
<p className="text-[10px] sm:text-xs text-orange-300 font-light tracking-wide leading-relaxed">
|
<p className="text-[10px] sm:text-xs text-orange-300 font-light tracking-wide leading-relaxed">
|
||||||
<strong className="text-orange-200">Note:</strong> You need to pay
|
<strong className="text-orange-200">20% deposit required</strong> to secure your booking. Pay the remaining balance on arrival at the hotel.
|
||||||
<strong className="text-[#d4af37]"> 20% deposit</strong> via
|
|
||||||
bank transfer immediately after booking to
|
|
||||||
secure the room. Pay the remaining balance
|
|
||||||
on arrival.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1769,14 +1765,14 @@ const BookingPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
Deposit to pay (20%)
|
Deposit to pay (20%)
|
||||||
</span>
|
</span>
|
||||||
<span className="text-base sm:text-lg font-serif font-semibold
|
<span className="text-base sm:text-lg font-display font-semibold
|
||||||
text-[#d4af37] tracking-tight"
|
text-[#d4af37] tracking-tight"
|
||||||
>
|
>
|
||||||
{formatPrice(totalPrice * 0.2)}
|
{formatPrice(totalPrice * 0.2)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] sm:text-xs text-orange-300/80 font-light tracking-wide">
|
<p className="text-[10px] sm:text-xs text-orange-300/80 font-light tracking-wide">
|
||||||
Pay via bank transfer to confirm booking
|
Pay 20% deposit to secure booking, remaining balance on arrival
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1791,15 +1787,12 @@ const BookingPage: React.FC = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{paymentMethod === 'cash' ? (
|
{paymentMethod === 'cash' ? (
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<Shield className="w-3.5 h-3.5 text-orange-400 mt-0.5 flex-shrink-0" />
|
<Shield className="w-3.5 h-3.5 text-orange-400 mt-0.5 flex-shrink-0" />
|
||||||
<p className="text-[10px] sm:text-xs text-orange-300 font-light tracking-wide leading-relaxed">
|
<p className="text-[10px] sm:text-xs text-orange-300 font-light tracking-wide leading-relaxed">
|
||||||
<strong className="text-orange-200">Required:</strong> Pay 20% deposit
|
<strong className="text-orange-200">20% deposit required</strong> to secure your booking. Pay the remaining balance ({formatPrice(totalPrice * 0.8)}) on arrival at the hotel.
|
||||||
via bank transfer after booking.
|
</p>
|
||||||
Remaining balance ({formatPrice(totalPrice * 0.8)})
|
</div>
|
||||||
to be paid on arrival.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<Sparkles className="w-3.5 h-3.5 text-[#d4af37] mt-0.5 flex-shrink-0" />
|
<Sparkles className="w-3.5 h-3.5 text-[#d4af37] mt-0.5 flex-shrink-0" />
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-green-200/80 font-light text-xs sm:text-sm tracking-wide">
|
<p className="text-green-200/80 font-light text-xs sm:text-sm tracking-wide">
|
||||||
Your booking has been confirmed.
|
Your booking has been confirmed.
|
||||||
Remaining amount to be paid at check-in.
|
Remaining amount to be paid on arrival at the hotel.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,12 +266,11 @@ const DepositPaymentPage: React.FC = () => {
|
|||||||
<CreditCard className="w-6 h-6 sm:w-7 sm:h-7 text-[#d4af37]" />
|
<CreditCard className="w-6 h-6 sm:w-7 sm:h-7 text-[#d4af37]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 text-center sm:text-left">
|
<div className="flex-1 text-center sm:text-left">
|
||||||
<h1 className="text-base sm:text-lg font-serif font-semibold text-[#d4af37] mb-1 tracking-wide">
|
<h1 className="text-base sm:text-lg font-elegant font-semibold text-[#d4af37] mb-1 tracking-wide">
|
||||||
Deposit Payment Required
|
Deposit Payment Required
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-300/80 font-light text-xs sm:text-sm tracking-wide">
|
<p className="text-gray-300/80 font-light text-xs sm:text-sm tracking-wide">
|
||||||
Please pay <strong className="text-[#d4af37] font-medium">20% deposit</strong> to
|
Please pay <strong className="text-[#d4af37] font-medium">20% deposit</strong> to confirm your booking. Pay the remaining balance on arrival at the hotel.
|
||||||
confirm your booking
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
type Booking,
|
type Booking,
|
||||||
} from '../../services/api/bookingService';
|
} from '../../services/api/bookingService';
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
import Loading from '../../components/common/Loading';
|
import Loading from '../../components/common/Loading';
|
||||||
import EmptyState from '../../components/common/EmptyState';
|
import EmptyState from '../../components/common/EmptyState';
|
||||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||||
@@ -31,6 +32,7 @@ import { parseDateLocal } from '../../utils/format';
|
|||||||
const MyBookingsPage: React.FC = () => {
|
const MyBookingsPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAuthenticated } = useAuthStore();
|
const { isAuthenticated } = useAuthStore();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
const { formatCurrency } = useFormatCurrency();
|
const { formatCurrency } = useFormatCurrency();
|
||||||
|
|
||||||
const [bookings, setBookings] = useState<Booking[]>([]);
|
const [bookings, setBookings] = useState<Booking[]>([]);
|
||||||
@@ -48,11 +50,9 @@ const MyBookingsPage: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
toast.error('Please login to view your bookings');
|
toast.error('Please login to view your bookings');
|
||||||
navigate('/login', {
|
openModal('login');
|
||||||
state: { from: '/bookings' }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, navigate]);
|
}, [isAuthenticated, openModal]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import { confirmBankTransfer } from
|
import { confirmBankTransfer } from
|
||||||
'../../services/api/paymentService';
|
'../../services/api/paymentService';
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
import Loading from '../../components/common/Loading';
|
import Loading from '../../components/common/Loading';
|
||||||
import PaymentStatusBadge from
|
import PaymentStatusBadge from
|
||||||
'../../components/common/PaymentStatusBadge';
|
'../../components/common/PaymentStatusBadge';
|
||||||
@@ -33,6 +34,7 @@ const PaymentConfirmationPage: React.FC = () => {
|
|||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAuthenticated } = useAuthStore();
|
const { isAuthenticated } = useAuthStore();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
const { formatCurrency } = useFormatCurrency();
|
const { formatCurrency } = useFormatCurrency();
|
||||||
|
|
||||||
const [booking, setBooking] = useState<Booking | null>(
|
const [booking, setBooking] = useState<Booking | null>(
|
||||||
@@ -54,9 +56,9 @@ const PaymentConfirmationPage: React.FC = () => {
|
|||||||
toast.error(
|
toast.error(
|
||||||
'Please login to confirm payment'
|
'Please login to confirm payment'
|
||||||
);
|
);
|
||||||
navigate('/login');
|
openModal('login');
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, navigate]);
|
}, [isAuthenticated, openModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id && isAuthenticated) {
|
if (id && isAuthenticated) {
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Shield className="w-3.5 h-3.5 text-[#d4af37] mt-0.5 flex-shrink-0" />
|
<Shield className="w-3.5 h-3.5 text-[#d4af37] mt-0.5 flex-shrink-0" />
|
||||||
<p className="text-[10px] sm:text-xs text-gray-300 font-light tracking-wide leading-relaxed">
|
<p className="text-[10px] sm:text-xs text-gray-300 font-light tracking-wide leading-relaxed">
|
||||||
No immediate charge — secure your booking now and pay at the hotel
|
<strong className="text-[#d4af37]">20% deposit required</strong> to secure your booking. Pay the remaining balance on arrival at the hotel.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
|
||||||
const rawBase = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
const rawBase = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||||
const normalized = String(rawBase).replace(/\/$/, '');
|
const normalized = String(rawBase).replace(/\/$/, '');
|
||||||
@@ -134,10 +135,21 @@ apiClient.interceptors.response.use(
|
|||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
localStorage.removeItem('userInfo');
|
localStorage.removeItem('userInfo');
|
||||||
|
|
||||||
|
// Update auth store state
|
||||||
|
useAuthStore.getState().logout().catch(() => {
|
||||||
|
// Ignore logout errors, just clear the state
|
||||||
|
useAuthStore.setState({
|
||||||
|
token: null,
|
||||||
|
userInfo: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (!window.location.pathname.includes('/login')) {
|
// Dispatch custom event to trigger login modal
|
||||||
window.location.href = '/login';
|
window.dispatchEvent(new CustomEvent('auth:logout', {
|
||||||
}
|
detail: { message: 'Session expired. Please login again.' }
|
||||||
|
}));
|
||||||
|
|
||||||
const errorMessage = (error.response?.data as any)?.message || 'Session expired. Please login again.';
|
const errorMessage = (error.response?.data as any)?.message || 'Session expired. Please login again.';
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
|
|||||||
@@ -90,14 +90,16 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
font-family: 'Poppins', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||||
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
|
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
|
||||||
'Droid Sans', 'Helvetica Neue', sans-serif;
|
'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-weight: 300;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
color: var(--luxury-gray-900);
|
color: var(--luxury-gray-900);
|
||||||
line-height: 1.6;
|
line-height: 1.7;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@@ -105,6 +107,42 @@ code {
|
|||||||
'Courier New', monospace;
|
'Courier New', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Luxury Typography */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: 'Cinzel', 'Playfair Display', serif;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3, h4, h5, h6 {
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Elegant text for special elements */
|
||||||
|
.elegant-text {
|
||||||
|
font-family: 'Cormorant Garamond', 'Playfair Display', serif;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display font for large headings */
|
||||||
|
.display-text {
|
||||||
|
font-family: 'Cinzel', 'Playfair Display', serif;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
|
||||||
.luxury-card {
|
.luxury-card {
|
||||||
@@ -143,6 +181,9 @@ code {
|
|||||||
@apply active:translate-y-0;
|
@apply active:translate-y-0;
|
||||||
@apply disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0;
|
@apply disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0;
|
||||||
@apply relative overflow-hidden;
|
@apply relative overflow-hidden;
|
||||||
|
font-family: 'Poppins', 'Inter', sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-luxury-primary::before {
|
.btn-luxury-primary::before {
|
||||||
@@ -171,14 +212,17 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.luxury-section-title {
|
.luxury-section-title {
|
||||||
@apply text-3xl md:text-4xl font-serif font-semibold;
|
@apply text-3xl md:text-4xl font-display font-semibold;
|
||||||
@apply text-gray-900 tracking-tight;
|
@apply text-gray-900 tracking-tight;
|
||||||
@apply mb-2;
|
@apply mb-2;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.luxury-section-subtitle {
|
.luxury-section-subtitle {
|
||||||
@apply text-gray-600 text-lg font-light;
|
@apply text-gray-600 text-lg font-light;
|
||||||
@apply tracking-wide;
|
@apply tracking-wide;
|
||||||
|
font-family: 'Cormorant Garamond', 'Playfair Display', serif;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -235,6 +279,8 @@ code {
|
|||||||
@apply bg-white text-gray-900;
|
@apply bg-white text-gray-900;
|
||||||
@apply placeholder:text-gray-400;
|
@apply placeholder:text-gray-400;
|
||||||
@apply font-light tracking-wide;
|
@apply font-light tracking-wide;
|
||||||
|
font-family: 'Poppins', 'Inter', sans-serif;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ export default {
|
|||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
serif: ['Playfair Display', 'Georgia', 'Times New Roman', 'serif'],
|
serif: ['Playfair Display', 'Georgia', 'Times New Roman', 'serif'],
|
||||||
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
display: ['Cinzel', 'Playfair Display', 'Georgia', 'serif'],
|
||||||
|
elegant: ['Cormorant Garamond', 'Playfair Display', 'Georgia', 'serif'],
|
||||||
|
sans: ['Poppins', 'Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
||||||
|
body: ['Poppins', 'Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
luxury: {
|
luxury: {
|
||||||
|
|||||||
Reference in New Issue
Block a user