updates
This commit is contained in:
@@ -307,22 +307,24 @@ export const notifyPayment = async (
|
||||
|
||||
export const generateQRCode = (
|
||||
bookingNumber: string,
|
||||
amount: number
|
||||
amount: number,
|
||||
bankCode?: string,
|
||||
accountNumber?: string,
|
||||
accountName?: string
|
||||
): string => {
|
||||
// If bank details are not provided, return empty string (QR code won't be generated)
|
||||
if (!bankCode || !accountNumber || !accountName) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
const bankCode = 'VCB';
|
||||
const accountNumber = '0123456789';
|
||||
const accountName = 'KHACH SAN ABC';
|
||||
const transferContent = bookingNumber;
|
||||
|
||||
|
||||
// Generate QR code URL (using generic QR code service, not Vietnam-specific)
|
||||
// Note: This is a placeholder - you may want to use a different QR code service
|
||||
// that supports international bank transfers
|
||||
const qrUrl =
|
||||
`https://img.vietqr.io/image/${bankCode}-${accountNumber}-compact2.jpg?` +
|
||||
`amount=${amount}&` +
|
||||
`addInfo=${encodeURIComponent(transferContent)}&` +
|
||||
`accountName=${encodeURIComponent(accountName)}`;
|
||||
`https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=` +
|
||||
encodeURIComponent(`Bank: ${bankCode}, Account: ${accountNumber}, Name: ${accountName}, Amount: ${amount}, Reference: ${transferContent}`);
|
||||
|
||||
return qrUrl;
|
||||
};
|
||||
|
||||
@@ -6,20 +6,26 @@ import {
|
||||
Phone,
|
||||
Mail,
|
||||
Linkedin,
|
||||
Twitter
|
||||
Twitter,
|
||||
Clock
|
||||
} from 'lucide-react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import pageContentService from '../services/pageContentService';
|
||||
import type { PageContent } from '../services/pageContentService';
|
||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { formatWorkingHours } from '../../../shared/utils/format';
|
||||
import { getThemeTextClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
const AboutPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const { theme } = useTheme();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [apiError, setApiError] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
@@ -194,7 +200,9 @@ const AboutPage: React.FC = () => {
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-full blur-3xl opacity-40 animate-pulse"></div>
|
||||
<div className="relative bg-gradient-to-br from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] p-6 rounded-2xl shadow-2xl shadow-[var(--luxury-gold)]/30">
|
||||
<Hotel className="w-12 h-12 md:w-16 md:h-16 text-white drop-shadow-lg" />
|
||||
{React.createElement(getIconComponent(pageContent?.about_hero_icon, Hotel), {
|
||||
className: 'w-12 h-12 md:w-16 md:h-16 text-white drop-shadow-lg'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,13 +453,15 @@ const AboutPage: React.FC = () => {
|
||||
<p className="text-[var(--luxury-gold)] font-medium mb-4 text-sm tracking-wide uppercase">{member.role}</p>
|
||||
{member.bio && <p className="text-gray-600 text-sm mb-6 leading-relaxed font-light">{member.bio}</p>}
|
||||
{member.social_links && (
|
||||
<div className="flex gap-4 pt-4 border-t border-gray-100">
|
||||
<div className={`flex gap-4 pt-4 border-t ${theme.theme_layout_mode === 'light' ? 'border-gray-200' : 'border-gray-700'}`}>
|
||||
{member.social_links.linkedin && (
|
||||
<a
|
||||
href={member.social_links.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110"
|
||||
className={`w-10 h-10 rounded-full ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gray-100 text-gray-700'
|
||||
: 'bg-gray-800 text-gray-300'} flex items-center justify-center hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110`}
|
||||
>
|
||||
<Linkedin className="w-5 h-5" />
|
||||
</a>
|
||||
@@ -461,7 +471,9 @@ const AboutPage: React.FC = () => {
|
||||
href={member.social_links.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110"
|
||||
className={`w-10 h-10 rounded-full ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gray-100 text-gray-700'
|
||||
: 'bg-gray-800 text-gray-300'} flex items-center justify-center hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110`}
|
||||
>
|
||||
<Twitter className="w-5 h-5" />
|
||||
</a>
|
||||
@@ -614,7 +626,9 @@ const AboutPage: React.FC = () => {
|
||||
{displayAddress && (
|
||||
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<MapPin className="w-8 h-8 text-white drop-shadow-md" />
|
||||
{React.createElement(getIconComponent(pageContent?.about_contact_icons?.location, MapPin), {
|
||||
className: 'w-8 h-8 text-white drop-shadow-md'
|
||||
})}
|
||||
</div>
|
||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
|
||||
Address
|
||||
@@ -633,7 +647,9 @@ const AboutPage: React.FC = () => {
|
||||
{displayPhone && (
|
||||
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<Phone className="w-8 h-8 text-white drop-shadow-md" />
|
||||
{React.createElement(getIconComponent(pageContent?.about_contact_icons?.phone, Phone), {
|
||||
className: 'w-8 h-8 text-white drop-shadow-md'
|
||||
})}
|
||||
</div>
|
||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
|
||||
Phone
|
||||
@@ -648,7 +664,9 @@ const AboutPage: React.FC = () => {
|
||||
{displayEmail && (
|
||||
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2" style={{ animationDelay: '0.2s' }}>
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<Mail className="w-8 h-8 text-white drop-shadow-md" />
|
||||
{React.createElement(getIconComponent(pageContent?.about_contact_icons?.email, Mail), {
|
||||
className: 'w-8 h-8 text-white drop-shadow-md'
|
||||
})}
|
||||
</div>
|
||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
|
||||
Email
|
||||
@@ -660,6 +678,19 @@ const AboutPage: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
|
||||
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2" style={{ animationDelay: '0.3s' }}>
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<Clock className="w-8 h-8 text-white drop-shadow-md" />
|
||||
</div>
|
||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
|
||||
Chat Support Hours
|
||||
</h3>
|
||||
<p className="text-gray-600 font-light">
|
||||
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center">
|
||||
@@ -668,7 +699,9 @@ const AboutPage: React.FC = () => {
|
||||
className="group inline-flex items-center space-x-3 px-10 py-4 bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] text-white rounded-xl hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/40 transition-all duration-500 font-medium text-lg tracking-wide relative overflow-hidden"
|
||||
>
|
||||
<span className="relative z-10">Explore Our Rooms</span>
|
||||
<Hotel className="w-5 h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300" />
|
||||
{React.createElement(getIconComponent(pageContent?.about_learn_more_icon, Hotel), {
|
||||
className: 'w-5 h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300'
|
||||
})}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[var(--luxury-gold-light)] to-[var(--luxury-gold)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,20 @@ import { Link } from 'react-router-dom';
|
||||
import pageContentService from '../services/pageContentService';
|
||||
import type { PageContent } from '../services/pageContentService';
|
||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
const AccessibilityPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const { theme } = useTheme();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
@@ -26,22 +33,26 @@ const AccessibilityPage: React.FC = () => {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = sanitizedContent;
|
||||
|
||||
// Get theme-aware colors
|
||||
const isLightMode = theme.theme_layout_mode === 'light';
|
||||
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
// Override inline colors to use theme-aware colors
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = headingColor;
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else {
|
||||
htmlEl.style.color = bodyColor;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -83,7 +94,7 @@ const AccessibilityPage: React.FC = () => {
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -95,8 +106,8 @@ const AccessibilityPage: React.FC = () => {
|
||||
>
|
||||
<div className="text-center">
|
||||
<Accessibility className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Accessibility</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Accessibility</h1>
|
||||
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||
@@ -110,7 +121,7 @@ const AccessibilityPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -133,38 +144,40 @@ const AccessibilityPage: React.FC = () => {
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||
<Accessibility className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
||||
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||
{pageContent.title || 'Accessibility'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||
prose-headings:font-elegant prose-headings:font-semibold
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:font-light prose-ul:my-4
|
||||
prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||
For accessibility inquiries, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||
{settings.company_email}
|
||||
|
||||
@@ -4,13 +4,20 @@ import { Calendar, User, Tag, ArrowLeft, Eye, Share2, ArrowRight } from 'lucide-
|
||||
import blogService, { BlogPost } from '../services/blogService';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
const BlogDetailPage: React.FC = () => {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { theme } = useTheme();
|
||||
const [post, setPost] = useState<BlogPost | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [relatedPosts, setRelatedPosts] = useState<BlogPost[]>([]);
|
||||
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
|
||||
useEffect(() => {
|
||||
if (slug) {
|
||||
@@ -108,9 +115,9 @@ const BlogDetailPage: React.FC = () => {
|
||||
|
||||
if (!post) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black text-white flex items-center justify-center">
|
||||
<div className={`min-h-screen ${bgClasses} flex items-center justify-center`}>
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Post not found</h1>
|
||||
<h1 className={`text-2xl font-bold mb-4 ${textClasses.primary}`}>Post not found</h1>
|
||||
<Link to="/blog" className="text-[var(--luxury-gold)] hover:underline">
|
||||
Back to Blog
|
||||
</Link>
|
||||
@@ -120,7 +127,7 @@ const BlogDetailPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
{/* Hero Section with Featured Image */}
|
||||
{post.featured_image && (
|
||||
<div className="relative w-full h-64 sm:h-80 lg:h-96 overflow-hidden">
|
||||
@@ -140,7 +147,7 @@ const BlogDetailPage: React.FC = () => {
|
||||
{/* Back Button */}
|
||||
<Link
|
||||
to="/blog"
|
||||
className="inline-flex items-center gap-2 text-gray-400 hover:text-[var(--luxury-gold)] transition-colors mb-8"
|
||||
className={`inline-flex items-center gap-2 ${textClasses.muted} hover:text-[var(--luxury-gold)] transition-colors mb-8`}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Blog</span>
|
||||
@@ -173,7 +180,7 @@ const BlogDetailPage: React.FC = () => {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-6 text-sm text-gray-400 pb-6 border-b border-[var(--luxury-gold)]/20">
|
||||
<div className={`flex flex-wrap items-center gap-6 text-sm ${textClasses.muted} pb-6 border-b border-[var(--luxury-gold)]/20`}>
|
||||
{post.published_at && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
@@ -201,19 +208,18 @@ const BlogDetailPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Article Content */}
|
||||
<article className="prose prose-invert prose-lg max-w-none mb-12
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
<article className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none mb-12
|
||||
prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-6 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-3
|
||||
prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-6
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-6
|
||||
prose-ol:text-gray-300 prose-ol:font-light prose-ol:my-6
|
||||
prose-li:text-gray-300 prose-li:mb-2
|
||||
prose-p:font-light prose-p:leading-relaxed prose-p:mb-6
|
||||
prose-ul:font-light prose-ul:my-6
|
||||
prose-ol:font-light prose-ol:my-6
|
||||
prose-li:mb-2
|
||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
||||
prose-img:rounded-xl prose-img:shadow-2xl
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white
|
||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
||||
prose-img:rounded-xl prose-img:shadow-2xl`}
|
||||
style={{ color: theme.theme_layout_mode === 'light' ? '#374151' : '#d1d5db' }}
|
||||
>
|
||||
<div
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
@@ -245,7 +251,7 @@ const BlogDetailPage: React.FC = () => {
|
||||
</h2>
|
||||
)}
|
||||
{section.content && (
|
||||
<p className="text-xl md:text-2xl text-gray-200 font-light leading-relaxed max-w-3xl mx-auto">
|
||||
<p className={`text-xl md:text-2xl ${textClasses.secondary} font-light leading-relaxed max-w-3xl mx-auto`}>
|
||||
{section.content}
|
||||
</p>
|
||||
)}
|
||||
@@ -277,8 +283,8 @@ const BlogDetailPage: React.FC = () => {
|
||||
<div className="rounded-3xl overflow-hidden border-2 border-[var(--luxury-gold)]/20 shadow-2xl">
|
||||
<img src={section.image} alt={section.title || 'Blog image'} className="w-full h-auto" />
|
||||
{section.title && (
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] px-6 py-4 border-t border-[var(--luxury-gold)]/20">
|
||||
<p className="text-gray-400 text-sm font-light italic text-center">{section.title}</p>
|
||||
<div className={`${cardClasses} px-6 py-4 border-t border-[var(--luxury-gold)]/20`}>
|
||||
<p className={`${textClasses.muted} text-sm font-light italic text-center`}>{section.title}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -373,7 +379,7 @@ const BlogDetailPage: React.FC = () => {
|
||||
{/* Related Posts */}
|
||||
{relatedPosts.length > 0 && (
|
||||
<div className="mt-16 pt-12 border-t border-[var(--luxury-gold)]/20">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-white mb-8">Related Posts</h2>
|
||||
<h2 className={`text-2xl sm:text-3xl font-bold ${textClasses.primary} mb-8`}>Related Posts</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{relatedPosts.map((relatedPost) => (
|
||||
<Link
|
||||
@@ -391,11 +397,11 @@ const BlogDetailPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-bold text-white mb-2 group-hover:text-[var(--luxury-gold)] transition-colors line-clamp-2">
|
||||
<h3 className={`text-lg font-bold ${textClasses.primary} mb-2 group-hover:text-[var(--luxury-gold)] transition-colors line-clamp-2`}>
|
||||
{relatedPost.title}
|
||||
</h3>
|
||||
{relatedPost.excerpt && (
|
||||
<p className="text-gray-400 text-sm line-clamp-2 font-light">
|
||||
<p className={`${textClasses.muted} text-sm line-clamp-2 font-light`}>
|
||||
{relatedPost.excerpt}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -3,9 +3,12 @@ import { Link } from 'react-router-dom';
|
||||
import { Calendar, User, Tag, Search, ArrowRight, Eye, Sparkles, BookOpen } from 'lucide-react';
|
||||
import blogService, { BlogPost } from '../services/blogService';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
|
||||
import Pagination from '../../../shared/components/Pagination';
|
||||
|
||||
const BlogPage: React.FC = () => {
|
||||
const { theme } = useTheme();
|
||||
const [posts, setPosts] = useState<BlogPost[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -67,10 +70,16 @@ const BlogPage: React.FC = () => {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
{/* Hero Section */}
|
||||
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8 overflow-hidden relative">
|
||||
<div className={`w-full ${heroBgClasses} pt-6 sm:pt-7 md:pt-8 overflow-hidden relative`}>
|
||||
{/* Background Effects */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
||||
@@ -83,18 +92,20 @@ const BlogPage: React.FC = () => {
|
||||
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
|
||||
<div className="relative p-2 sm:p-2.5 md:p-3 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300">
|
||||
<div className={`relative p-2 sm:p-2.5 md:p-3 ${cardClasses} rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300`}>
|
||||
<BookOpen className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
|
||||
<span className="bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
||||
<span className={`${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||
Our Blog
|
||||
</span>
|
||||
</h1>
|
||||
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
|
||||
<p className="text-sm sm:text-base md:text-lg text-gray-300 font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4">
|
||||
<p className={`text-sm sm:text-base md:text-lg ${textClasses.secondary} font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4`}>
|
||||
Discover stories, insights, and updates from our luxury hotel
|
||||
</p>
|
||||
<div className="mt-4 flex items-center justify-center gap-2 text-[var(--luxury-gold)]/60">
|
||||
@@ -125,7 +136,7 @@ const BlogPage: React.FC = () => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="w-full pl-14 pr-5 py-4 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] border border-[var(--luxury-gold)]/20 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light"
|
||||
className={`w-full pl-14 pr-5 py-4 ${inputClasses} border border-[var(--luxury-gold)]/20 rounded-xl placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,7 +150,7 @@ const BlogPage: React.FC = () => {
|
||||
|
||||
{/* Results Count */}
|
||||
{!loading && total > 0 && (
|
||||
<div className="mb-8 text-gray-400 font-light text-sm">
|
||||
<div className={`mb-8 ${textClasses.muted} font-light text-sm`}>
|
||||
Showing {posts.length} of {total} posts
|
||||
</div>
|
||||
)}
|
||||
@@ -150,8 +161,8 @@ const BlogPage: React.FC = () => {
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--luxury-gold)]/10 mb-6">
|
||||
<BookOpen className="w-10 h-10 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<p className="text-gray-400 text-xl font-light">No blog posts found</p>
|
||||
<p className="text-gray-500 text-sm mt-2">Try adjusting your search or filters</p>
|
||||
<p className={`${textClasses.muted} text-xl font-light`}>No blog posts found</p>
|
||||
<p className={`${textClasses.muted} text-sm mt-2`}>Try adjusting your search or filters</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -160,7 +171,7 @@ const BlogPage: React.FC = () => {
|
||||
<Link
|
||||
key={post.id}
|
||||
to={`/blog/${post.slug}`}
|
||||
className="group relative bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3"
|
||||
className={`group relative ${cardClasses} rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3`}
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
{/* Premium Glow Effects */}
|
||||
@@ -204,15 +215,15 @@ const BlogPage: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-2xl sm:text-3xl font-serif font-bold text-white mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight">
|
||||
<h2 className={`text-2xl sm:text-3xl font-serif font-bold ${textClasses.primary} mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight`}>
|
||||
{post.title}
|
||||
</h2>
|
||||
{post.excerpt && (
|
||||
<p className="text-gray-300 text-base mb-6 line-clamp-3 font-light leading-relaxed">
|
||||
<p className={`${textClasses.secondary} text-base mb-6 line-clamp-3 font-light leading-relaxed`}>
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center justify-between text-sm text-gray-400 pt-6 border-t border-[var(--luxury-gold)]/20 mb-6">
|
||||
<div className={`flex items-center justify-between text-sm ${textClasses.muted} pt-6 border-t border-[var(--luxury-gold)]/20 mb-6`}>
|
||||
<div className="flex items-center gap-4">
|
||||
{post.published_at && (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -259,10 +270,10 @@ const BlogPage: React.FC = () => {
|
||||
<div className="lg:col-span-3">
|
||||
{allTags.length > 0 && (
|
||||
<div className="sticky top-8">
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 backdrop-blur-xl shadow-2xl">
|
||||
<div className={`${cardClasses} rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 backdrop-blur-xl shadow-2xl`}>
|
||||
<div className="flex items-center gap-3 mb-6 pb-6 border-b border-[var(--luxury-gold)]/20">
|
||||
<div className="w-1 h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
|
||||
<h3 className="text-xl font-serif font-bold text-white">Filter by Tags</h3>
|
||||
<h3 className={`text-xl font-serif font-bold ${textClasses.primary}`}>Filter by Tags</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
@@ -273,7 +284,7 @@ const BlogPage: React.FC = () => {
|
||||
className={`group relative w-full text-left px-5 py-4 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
||||
selectedTag === null
|
||||
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
||||
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] hover:bg-[#1a1a1a]'
|
||||
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
|
||||
}`}
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
@@ -294,7 +305,7 @@ const BlogPage: React.FC = () => {
|
||||
className={`group relative w-full text-left px-5 py-4 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
||||
selectedTag === tag
|
||||
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
||||
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] hover:bg-[#1a1a1a]'
|
||||
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
|
||||
}`}
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
|
||||
@@ -5,11 +5,18 @@ import pageContentService, { PageContent } from '../services/pageContentService'
|
||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
const CancellationPolicyPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const { theme } = useTheme();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
@@ -26,22 +33,27 @@ const CancellationPolicyPage: React.FC = () => {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = sanitizedContent;
|
||||
|
||||
// Get theme-aware colors
|
||||
const isLightMode = theme.theme_layout_mode === 'light';
|
||||
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
// Override inline colors to use theme-aware colors
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = headingColor;
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else {
|
||||
htmlEl.style.color = bodyColor;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -83,7 +95,7 @@ const CancellationPolicyPage: React.FC = () => {
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -95,8 +107,8 @@ const CancellationPolicyPage: React.FC = () => {
|
||||
>
|
||||
<div className="text-center">
|
||||
<XCircle className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Cancellation Policy</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Cancellation Policy</h1>
|
||||
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||
@@ -110,7 +122,7 @@ const CancellationPolicyPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -133,38 +145,42 @@ const CancellationPolicyPage: React.FC = () => {
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||
<XCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
||||
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||
{pageContent.title || 'Cancellation Policy'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||
prose-headings:font-elegant prose-headings:font-semibold
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:font-light prose-ul:my-4
|
||||
prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||
style={{
|
||||
color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db',
|
||||
}}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||
For questions about cancellations, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||
{settings.company_email}
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Mail, Phone, MapPin, Send, User, MessageSquare } from 'lucide-react';
|
||||
import { Mail, Phone, MapPin, Send, User, MessageSquare, Clock } from 'lucide-react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { submitContactForm } from '../services/contactService';
|
||||
import pageContentService from '../services/pageContentService';
|
||||
import type { PageContent } from '../services/pageContentService';
|
||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import { toast } from 'react-toastify';
|
||||
import Recaptcha from '../../../shared/components/Recaptcha';
|
||||
import { recaptchaService } from '../../../features/system/services/systemSettingsService';
|
||||
import ChatWidget from '../../notifications/components/ChatWidget';
|
||||
import { useAntibotForm } from '../../../features/auth/hooks/useAntibotForm';
|
||||
import HoneypotField from '../../../shared/components/HoneypotField';
|
||||
import { formatWorkingHours } from '../../../shared/utils/format';
|
||||
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
// Helper function to get icon component from icon name (handles both PascalCase and lowercase)
|
||||
const getIconComponent = (iconName?: string, fallback: any = Mail) => {
|
||||
if (!iconName) return fallback;
|
||||
|
||||
// Try direct match first (for PascalCase names)
|
||||
if ((LucideIcons as any)[iconName]) {
|
||||
return (LucideIcons as any)[iconName];
|
||||
}
|
||||
|
||||
// Convert to PascalCase (capitalize first letter)
|
||||
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
|
||||
if ((LucideIcons as any)[pascalCaseName]) {
|
||||
return (LucideIcons as any)[pascalCaseName];
|
||||
}
|
||||
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const ContactPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const { theme } = useTheme();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
@@ -153,9 +176,9 @@ const ContactPage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
|
||||
const displayPhone = settings.company_phone || 'Available 24/7 for your convenience';
|
||||
const displayEmail = settings.company_email || "We'll respond within 24 hours";
|
||||
const displayAddress = settings.company_address || 'Visit us at our hotel reception';
|
||||
const displayPhone = settings.company_phone || null;
|
||||
const displayEmail = settings.company_email || null;
|
||||
const displayAddress = settings.company_address || null;
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
@@ -171,10 +194,16 @@ const ContactPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
{}
|
||||
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8 overflow-hidden relative">
|
||||
<div className={`w-full ${heroBgClasses} pt-6 sm:pt-7 md:pt-8 overflow-hidden relative`}>
|
||||
{}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
||||
@@ -187,18 +216,22 @@ const ContactPage: React.FC = () => {
|
||||
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
|
||||
<div className="relative p-2 sm:p-2.5 md:p-3 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300">
|
||||
<Mail className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
<div className={`relative p-2 sm:p-2.5 md:p-3 ${cardClasses} rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300`}>
|
||||
{React.createElement(getIconComponent(pageContent?.contact_icons?.hero, Mail), {
|
||||
className: 'w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
|
||||
<span className="bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
||||
<h1 className={`text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2`}>
|
||||
<span className={theme.theme_layout_mode === 'light'
|
||||
? "bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900 bg-clip-text text-transparent"
|
||||
: "bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent"}>
|
||||
{pageContent?.title || 'Contact Us'}
|
||||
</span>
|
||||
</h1>
|
||||
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
|
||||
<p className="text-sm sm:text-base md:text-lg text-gray-300 font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4">
|
||||
<p className={`text-sm sm:text-base md:text-lg ${textClasses.secondary} font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4`}>
|
||||
{pageContent?.subtitle || pageContent?.description || "Experience the pinnacle of hospitality. We're here to make your stay extraordinary."}
|
||||
</p>
|
||||
</div>
|
||||
@@ -212,18 +245,18 @@ const ContactPage: React.FC = () => {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 sm:gap-5 md:gap-6 lg:gap-7 xl:gap-8 2xl:gap-10 max-w-7xl mx-auto">
|
||||
{}
|
||||
<div className="lg:col-span-4">
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a]
|
||||
<div className={`${cardClasses}
|
||||
rounded-xl sm:rounded-2xl border-2 border-[var(--luxury-gold)]/30 p-5 sm:p-6 md:p-8 lg:p-10
|
||||
shadow-2xl shadow-[var(--luxury-gold)]/10 backdrop-blur-xl
|
||||
relative overflow-hidden h-full group hover:border-[var(--luxury-gold)]/50 transition-all duration-500">
|
||||
relative overflow-hidden h-full group hover:border-[var(--luxury-gold)]/50 transition-all duration-500`}>
|
||||
{}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)]/5 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8">
|
||||
<div className="w-0.5 sm:w-1 h-6 sm:h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
|
||||
<h2 className="text-xl sm:text-2xl md:text-3xl font-serif font-semibold
|
||||
text-white tracking-tight">
|
||||
<h2 className={`text-xl sm:text-2xl md:text-3xl font-serif font-semibold
|
||||
${textClasses.primary} tracking-tight`}>
|
||||
Get in Touch
|
||||
</h2>
|
||||
</div>
|
||||
@@ -231,11 +264,13 @@ const ContactPage: React.FC = () => {
|
||||
<div className="space-y-5 sm:space-y-6 md:space-y-7">
|
||||
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
||||
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
|
||||
<Mail className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
{React.createElement(getIconComponent(pageContent?.contact_icons?.email, Mail), {
|
||||
className: 'w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Email</h3>
|
||||
<a href={`mailto:${displayEmail}`} className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors">
|
||||
<a href={`mailto:${displayEmail}`} className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors`}>
|
||||
{displayEmail}
|
||||
</a>
|
||||
</div>
|
||||
@@ -243,11 +278,13 @@ const ContactPage: React.FC = () => {
|
||||
|
||||
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
||||
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
|
||||
<Phone className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
{React.createElement(getIconComponent(pageContent?.contact_icons?.phone, Phone), {
|
||||
className: 'w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Phone</h3>
|
||||
<a href={`tel:${displayPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`} className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors">
|
||||
<a href={`tel:${displayPhone?.replace(/\s+/g, '').replace(/[()]/g, '') || ''}`} className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors`}>
|
||||
{displayPhone}
|
||||
</a>
|
||||
</div>
|
||||
@@ -255,15 +292,31 @@ const ContactPage: React.FC = () => {
|
||||
|
||||
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
||||
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
|
||||
<MapPin className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
{React.createElement(getIconComponent(pageContent?.contact_icons?.location, MapPin), {
|
||||
className: 'w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Location</h3>
|
||||
<p className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed whitespace-pre-line">
|
||||
<p className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed whitespace-pre-line`}>
|
||||
{displayAddress}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
|
||||
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
||||
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
|
||||
<Clock className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Chat Support Hours</h3>
|
||||
<p className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed`}>
|
||||
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{}
|
||||
@@ -289,7 +342,7 @@ const ContactPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
<div className="mt-5 sm:mt-6 md:mt-7 pt-5 sm:pt-6 md:pt-7 border-t border-[var(--luxury-gold)]/30">
|
||||
<p className="text-gray-400 font-light text-xs sm:text-sm leading-relaxed tracking-wide">
|
||||
<p className={`${textClasses.muted} font-light text-xs sm:text-sm leading-relaxed tracking-wide`}>
|
||||
{pageContent?.content || "Our team is here to help you with any questions about your stay, bookings, or special requests. We're committed to exceeding your expectations."}
|
||||
</p>
|
||||
</div>
|
||||
@@ -299,10 +352,10 @@ const ContactPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
<div className="lg:col-span-8">
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a]
|
||||
<div className={`${cardClasses}
|
||||
rounded-xl sm:rounded-2xl border-2 border-[var(--luxury-gold)]/30 p-5 sm:p-6 md:p-8 lg:p-10
|
||||
shadow-2xl shadow-[var(--luxury-gold)]/10 backdrop-blur-xl
|
||||
relative overflow-hidden">
|
||||
relative overflow-hidden`}>
|
||||
{}
|
||||
<div className="absolute inset-0 opacity-5">
|
||||
<div className="absolute top-0 right-0 w-48 sm:w-64 md:w-96 h-48 sm:h-64 md:h-96 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
||||
@@ -311,8 +364,8 @@ const ContactPage: React.FC = () => {
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8">
|
||||
<div className="w-0.5 sm:w-1 h-6 sm:h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
|
||||
<h2 className="text-xl sm:text-2xl md:text-3xl font-serif font-semibold
|
||||
text-white tracking-tight">
|
||||
<h2 className={`text-xl sm:text-2xl md:text-3xl font-serif font-semibold
|
||||
${textClasses.primary} tracking-tight`}>
|
||||
Send Us a Message
|
||||
</h2>
|
||||
</div>
|
||||
@@ -328,9 +381,11 @@ const ContactPage: React.FC = () => {
|
||||
)}
|
||||
{}
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
||||
<label htmlFor="name" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
|
||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||
<User className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
{React.createElement(getIconComponent(pageContent?.contact_icons?.name_field, User), {
|
||||
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||
})}
|
||||
Full Name <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
||||
</span>
|
||||
</label>
|
||||
@@ -340,8 +395,8 @@ const ContactPage: React.FC = () => {
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
|
||||
text-sm sm:text-base placeholder-gray-500/60
|
||||
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
||||
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
||||
${errors.name ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
||||
@@ -356,9 +411,11 @@ const ContactPage: React.FC = () => {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-5 md:gap-6 lg:gap-7">
|
||||
{}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
||||
<label htmlFor="email" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
|
||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||
<Mail className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
{React.createElement(getIconComponent(pageContent?.contact_icons?.email_field, Mail), {
|
||||
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||
})}
|
||||
Email <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
||||
</span>
|
||||
</label>
|
||||
@@ -368,8 +425,8 @@ const ContactPage: React.FC = () => {
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
|
||||
text-sm sm:text-base placeholder-gray-500/60
|
||||
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
||||
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
||||
${errors.email ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
||||
@@ -382,10 +439,12 @@ const ContactPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
||||
<label htmlFor="phone" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
|
||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||
<Phone className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
Phone <span className="text-gray-500 text-xs">(Optional)</span>
|
||||
{React.createElement(getIconComponent(pageContent?.contact_icons?.phone_field, Phone), {
|
||||
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||
})}
|
||||
Phone <span className={`${textClasses.muted} text-xs`}>(Optional)</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -394,10 +453,10 @@ const ContactPage: React.FC = () => {
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 border-[var(--luxury-gold)]/30 rounded-lg
|
||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 border-[var(--luxury-gold)]/30 rounded-lg
|
||||
text-sm sm:text-base placeholder-gray-500/60
|
||||
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
||||
transition-all duration-300 hover:border-[var(--luxury-gold)]/40"
|
||||
transition-all duration-300 hover:border-[var(--luxury-gold)]/40`}
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
@@ -405,9 +464,11 @@ const ContactPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
<div>
|
||||
<label htmlFor="subject" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
||||
<label htmlFor="subject" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
|
||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
{React.createElement(getIconComponent(pageContent?.contact_icons?.subject_field, MessageSquare), {
|
||||
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||
})}
|
||||
Subject <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
||||
</span>
|
||||
</label>
|
||||
@@ -417,8 +478,8 @@ const ContactPage: React.FC = () => {
|
||||
name="subject"
|
||||
value={formData.subject}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
|
||||
text-sm sm:text-base placeholder-gray-500/60
|
||||
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
||||
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
||||
${errors.subject ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
||||
@@ -431,9 +492,11 @@ const ContactPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
||||
<label htmlFor="message" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
|
||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
{React.createElement(getIconComponent(pageContent?.contact_icons?.message_field, MessageSquare), {
|
||||
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||
})}
|
||||
Message <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
||||
</span>
|
||||
</label>
|
||||
@@ -443,8 +506,8 @@ const ContactPage: React.FC = () => {
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
rows={6}
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
||||
text-white text-sm sm:text-base placeholder-gray-500/60 resize-none
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
|
||||
text-sm sm:text-base placeholder-gray-500/60 resize-none
|
||||
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
||||
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
||||
${errors.message ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
||||
@@ -494,7 +557,9 @@ const ContactPage: React.FC = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-4 h-4 sm:w-5 sm:h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300" />
|
||||
{React.createElement(getIconComponent(pageContent?.contact_icons?.submit_button, Send), {
|
||||
className: 'w-4 h-4 sm:w-5 sm:h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300'
|
||||
})}
|
||||
<span className="relative z-10">Send Message</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -4,13 +4,20 @@ import { Link } from 'react-router-dom';
|
||||
import pageContentService from '../services/pageContentService';
|
||||
import type { PageContent } from '../services/pageContentService';
|
||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
const FAQPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const { theme } = useTheme();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
@@ -26,22 +33,26 @@ const FAQPage: React.FC = () => {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = sanitizedContent;
|
||||
|
||||
// Get theme-aware colors
|
||||
const isLightMode = theme.theme_layout_mode === 'light';
|
||||
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||
|
||||
// Override inline colors to use theme-aware colors
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = headingColor;
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else {
|
||||
htmlEl.style.color = bodyColor;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -83,7 +94,7 @@ const FAQPage: React.FC = () => {
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -95,8 +106,8 @@ const FAQPage: React.FC = () => {
|
||||
>
|
||||
<div className="text-center">
|
||||
<HelpCircle className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Frequently Asked Questions</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Frequently Asked Questions</h1>
|
||||
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||
@@ -110,7 +121,7 @@ const FAQPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -133,38 +144,40 @@ const FAQPage: React.FC = () => {
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||
<HelpCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
||||
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900 bg-clip-text text-transparent'
|
||||
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent'}`}>
|
||||
{pageContent.title || 'Frequently Asked Questions'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||
prose-headings:font-elegant prose-headings:font-semibold
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:font-light prose-ul:my-4
|
||||
prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||
Still have questions? Contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||
{settings.company_email}
|
||||
|
||||
@@ -29,6 +29,24 @@ import type { Room } from '../../rooms/services/roomService';
|
||||
import type { PageContent } from '../services/pageContentService';
|
||||
import type { Service } from '../../hotel_services/services/serviceService';
|
||||
|
||||
// Helper function to get icon component from icon name (handles both PascalCase and lowercase)
|
||||
const getIconComponent = (iconName?: string) => {
|
||||
if (!iconName) return null;
|
||||
|
||||
// Try direct match first (for PascalCase names)
|
||||
if ((LucideIcons as any)[iconName]) {
|
||||
return (LucideIcons as any)[iconName];
|
||||
}
|
||||
|
||||
// Convert to PascalCase (capitalize first letter)
|
||||
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
|
||||
if ((LucideIcons as any)[pascalCaseName]) {
|
||||
return (LucideIcons as any)[pascalCaseName];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
@@ -1081,31 +1099,34 @@ const HomePage: React.FC = () => {
|
||||
)}
|
||||
|
||||
<div className="relative grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6 lg:gap-8">
|
||||
{pageContent.stats.map((stat, index) => (
|
||||
<div key={`stat-${index}-${stat.label || index}`} className="text-center group relative">
|
||||
{stat?.icon && (
|
||||
<div className="mb-3 md:mb-4 group-hover:scale-110 transition-transform duration-300 flex items-center justify-center">
|
||||
{stat.icon && (LucideIcons as any)[stat.icon] ? (
|
||||
React.createElement((LucideIcons as any)[stat.icon], {
|
||||
className: 'w-8 h-8 md:w-10 md:h-10 text-[var(--luxury-gold)] drop-shadow-md'
|
||||
})
|
||||
) : (
|
||||
<span className="text-3xl md:text-4xl">{stat.icon}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{stat?.number && (
|
||||
<div className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--luxury-gold)] mb-1 md:mb-2 font-serif tracking-tight">
|
||||
{stat.number}
|
||||
</div>
|
||||
)}
|
||||
{stat?.label && (
|
||||
<div className="text-gray-300 text-xs sm:text-sm md:text-base font-light tracking-wider uppercase">
|
||||
{stat.label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{pageContent.stats.map((stat, index) => {
|
||||
const IconComponent = stat?.icon ? getIconComponent(stat.icon) : null;
|
||||
return (
|
||||
<div key={`stat-${index}-${stat.label || index}`} className="text-center group relative">
|
||||
{stat?.icon && (
|
||||
<div className="mb-3 md:mb-4 group-hover:scale-110 transition-transform duration-300 flex items-center justify-center">
|
||||
{IconComponent ? (
|
||||
React.createElement(IconComponent, {
|
||||
className: 'w-8 h-8 md:w-10 md:h-10 text-[var(--luxury-gold)] drop-shadow-md'
|
||||
})
|
||||
) : (
|
||||
<span className="text-3xl md:text-4xl">✨</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{stat?.number && (
|
||||
<div className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--luxury-gold)] mb-1 md:mb-2 font-serif tracking-tight">
|
||||
{stat.number}
|
||||
</div>
|
||||
)}
|
||||
{stat?.label && (
|
||||
<div className="text-gray-300 text-xs sm:text-sm md:text-base font-light tracking-wider uppercase">
|
||||
{stat.label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -4,13 +4,20 @@ import { Link } from 'react-router-dom';
|
||||
import pageContentService from '../services/pageContentService';
|
||||
import type { PageContent } from '../services/pageContentService';
|
||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
const PrivacyPolicyPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const { theme } = useTheme();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
@@ -28,24 +35,26 @@ const PrivacyPolicyPage: React.FC = () => {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = sanitizedContent;
|
||||
|
||||
// Add color styles to elements that don't have them
|
||||
// Get theme-aware colors
|
||||
const isLightMode = theme.theme_layout_mode === 'light';
|
||||
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||
|
||||
// Override inline colors to use theme-aware colors
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
// Only add color if not already set
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = headingColor;
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else {
|
||||
htmlEl.style.color = bodyColor;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,7 +97,7 @@ const PrivacyPolicyPage: React.FC = () => {
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -100,8 +109,8 @@ const PrivacyPolicyPage: React.FC = () => {
|
||||
>
|
||||
<div className="text-center">
|
||||
<Shield className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Privacy Policy</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Privacy Policy</h1>
|
||||
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||
@@ -115,7 +124,7 @@ const PrivacyPolicyPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -140,32 +149,34 @@ const PrivacyPolicyPage: React.FC = () => {
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||
<Shield className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
||||
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||
{pageContent.title || 'Privacy Policy'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||
prose-headings:font-elegant prose-headings:font-semibold
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:font-light prose-ul:my-4
|
||||
prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -174,7 +185,7 @@ const PrivacyPolicyPage: React.FC = () => {
|
||||
<div className="mt-8 space-y-4">
|
||||
{settings.company_email && (
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||
For questions about this policy, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||
{settings.company_email}
|
||||
|
||||
@@ -4,13 +4,20 @@ import { Link } from 'react-router-dom';
|
||||
import pageContentService from '../services/pageContentService';
|
||||
import type { PageContent } from '../services/pageContentService';
|
||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
const RefundsPolicyPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const { theme } = useTheme();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
@@ -28,24 +35,26 @@ const RefundsPolicyPage: React.FC = () => {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = sanitizedContent;
|
||||
|
||||
// Add color styles to elements that don't have them
|
||||
// Get theme-aware colors
|
||||
const isLightMode = theme.theme_layout_mode === 'light';
|
||||
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||
|
||||
// Override inline colors to use theme-aware colors
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
// Only add color if not already set
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = headingColor;
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else {
|
||||
htmlEl.style.color = bodyColor;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,7 +97,7 @@ const RefundsPolicyPage: React.FC = () => {
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -100,8 +109,8 @@ const RefundsPolicyPage: React.FC = () => {
|
||||
>
|
||||
<div className="text-center">
|
||||
<RefreshCw className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Refunds Policy</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Refunds Policy</h1>
|
||||
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||
@@ -115,7 +124,7 @@ const RefundsPolicyPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -140,32 +149,34 @@ const RefundsPolicyPage: React.FC = () => {
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||
<RefreshCw className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
||||
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||
{pageContent.title || 'Refunds Policy'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||
prose-headings:font-elegant prose-headings:font-semibold
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:font-light prose-ul:my-4
|
||||
prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -173,7 +184,7 @@ const RefundsPolicyPage: React.FC = () => {
|
||||
{/* Footer Note */}
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||
For refund inquiries, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||
{settings.company_email}
|
||||
|
||||
@@ -7,6 +7,8 @@ import serviceService from '../../hotel_services/services/serviceService';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
interface ServiceSection {
|
||||
type: 'hero' | 'text' | 'image' | 'gallery' | 'quote' | 'features' | 'cta' | 'video';
|
||||
@@ -47,11 +49,16 @@ interface ServiceDetail {
|
||||
const ServiceDetailPage: React.FC = () => {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { theme } = useTheme();
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
const [service, setService] = useState<ServiceDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [relatedServices, setRelatedServices] = useState<ServiceDetail[]>([]);
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
|
||||
useEffect(() => {
|
||||
if (slug) {
|
||||
@@ -288,9 +295,9 @@ const ServiceDetailPage: React.FC = () => {
|
||||
|
||||
if (!service) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black text-white flex items-center justify-center">
|
||||
<div className={`min-h-screen ${bgClasses} flex items-center justify-center`}>
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Service not found</h1>
|
||||
<h1 className={`text-2xl font-bold mb-4 ${textClasses.primary}`}>Service not found</h1>
|
||||
<Link to="/services" className="text-[var(--luxury-gold)] hover:underline">
|
||||
Back to Services
|
||||
</Link>
|
||||
@@ -300,7 +307,7 @@ const ServiceDetailPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
{/* Hero Section with Featured Image - Enhanced Luxury */}
|
||||
{service.image && (
|
||||
<div className="relative w-full h-[60vh] min-h-[500px] max-h-[800px] overflow-hidden">
|
||||
@@ -338,11 +345,11 @@ const ServiceDetailPage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-serif font-bold text-white mb-6 leading-tight drop-shadow-2xl">
|
||||
<h1 className={`text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-serif font-bold ${textClasses.primary} mb-6 leading-tight drop-shadow-2xl`}>
|
||||
{service.title}
|
||||
</h1>
|
||||
{service.description && (
|
||||
<p className="text-xl sm:text-2xl lg:text-3xl text-gray-200 font-light leading-relaxed max-w-4xl mb-8 drop-shadow-lg">
|
||||
<p className={`text-xl sm:text-2xl lg:text-3xl ${textClasses.secondary} font-light leading-relaxed max-w-4xl mb-8 drop-shadow-lg`}>
|
||||
{service.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -352,7 +359,7 @@ const ServiceDetailPage: React.FC = () => {
|
||||
{formatCurrency(service.price)}
|
||||
</span>
|
||||
{service.unit && (
|
||||
<span className="text-xl sm:text-2xl text-gray-300 font-light">
|
||||
<span className={`text-xl sm:text-2xl ${textClasses.secondary} font-light`}>
|
||||
/ {service.unit}
|
||||
</span>
|
||||
)}
|
||||
@@ -375,7 +382,7 @@ const ServiceDetailPage: React.FC = () => {
|
||||
{!service.image && (
|
||||
<Link
|
||||
to="/services"
|
||||
className="inline-flex items-center gap-3 px-6 py-3 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] border-2 border-[var(--luxury-gold)]/20 rounded-xl text-gray-300 hover:text-[var(--luxury-gold)] hover:border-[var(--luxury-gold)]/50 transition-all duration-300 mb-12 group"
|
||||
className={`inline-flex items-center gap-3 px-6 py-3 ${cardClasses} border-2 border-[var(--luxury-gold)]/20 rounded-xl ${textClasses.secondary} hover:text-[var(--luxury-gold)] hover:border-[var(--luxury-gold)]/50 transition-all duration-300 mb-12 group`}
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
|
||||
<span className="font-medium">Back to Services</span>
|
||||
@@ -422,7 +429,7 @@ const ServiceDetailPage: React.FC = () => {
|
||||
{formatCurrency(service.price)}
|
||||
</span>
|
||||
{service.unit && (
|
||||
<span className="text-xl text-gray-400 font-light">
|
||||
<span className={`text-xl ${textClasses.muted} font-light`}>
|
||||
/ {service.unit}
|
||||
</span>
|
||||
)}
|
||||
@@ -513,12 +520,12 @@ const ServiceDetailPage: React.FC = () => {
|
||||
</div>
|
||||
<div className="relative px-8 py-20 md:px-16 md:py-32 text-center">
|
||||
{section.title && (
|
||||
<h2 className="text-5xl md:text-6xl lg:text-7xl font-serif font-bold text-white mb-8 leading-tight drop-shadow-2xl">
|
||||
<h2 className={`text-5xl md:text-6xl lg:text-7xl font-serif font-bold ${textClasses.primary} mb-8 leading-tight drop-shadow-2xl`}>
|
||||
{section.title}
|
||||
</h2>
|
||||
)}
|
||||
{section.content && (
|
||||
<p className="text-2xl md:text-3xl text-gray-200 font-light leading-relaxed max-w-4xl mx-auto drop-shadow-lg">
|
||||
<p className={`text-2xl md:text-3xl ${textClasses.secondary} font-light leading-relaxed max-w-4xl mx-auto drop-shadow-lg`}>
|
||||
{section.content}
|
||||
</p>
|
||||
)}
|
||||
@@ -529,17 +536,17 @@ const ServiceDetailPage: React.FC = () => {
|
||||
|
||||
{/* Text Section */}
|
||||
{section.type === 'text' && (
|
||||
<div className={`bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 md:p-12 shadow-xl ${
|
||||
<div className={`${cardClasses} rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 md:p-12 shadow-xl ${
|
||||
section.alignment === 'center' ? 'text-center' : section.alignment === 'right' ? 'text-right' : 'text-left'
|
||||
}`}>
|
||||
{section.title && (
|
||||
<h3 className="text-3xl md:text-4xl font-serif font-bold text-white mb-6">
|
||||
<h3 className={`text-3xl md:text-4xl font-serif font-bold ${textClasses.primary} mb-6`}>
|
||||
{section.title}
|
||||
</h3>
|
||||
)}
|
||||
{section.content && (
|
||||
<div
|
||||
className="text-gray-300 font-light leading-relaxed text-lg"
|
||||
className={`${textClasses.secondary} font-light leading-relaxed text-lg`}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(section.content)}
|
||||
/>
|
||||
)}
|
||||
@@ -551,8 +558,8 @@ const ServiceDetailPage: React.FC = () => {
|
||||
<div className="rounded-3xl overflow-hidden border-2 border-[var(--luxury-gold)]/20 shadow-2xl">
|
||||
<img src={section.image} alt={section.title || 'Service image'} className="w-full h-auto" />
|
||||
{section.title && (
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] px-6 py-4 border-t border-[var(--luxury-gold)]/20">
|
||||
<p className="text-gray-400 text-sm font-light italic text-center">{section.title}</p>
|
||||
<div className={`${cardClasses} px-6 py-4 border-t border-[var(--luxury-gold)]/20`}>
|
||||
<p className={`${textClasses.muted} text-sm font-light italic text-center`}>{section.title}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -666,7 +673,7 @@ const ServiceDetailPage: React.FC = () => {
|
||||
{/* Related Services */}
|
||||
{relatedServices.length > 0 && (
|
||||
<div className="mt-16 pt-12 border-t border-[var(--luxury-gold)]/20">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-white mb-8">Related Services</h2>
|
||||
<h2 className={`text-2xl sm:text-3xl font-bold ${textClasses.primary} mb-8`}>Related Services</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{relatedServices.map((relatedService) => (
|
||||
<Link
|
||||
@@ -684,11 +691,11 @@ const ServiceDetailPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-bold text-white mb-2 group-hover:text-[var(--luxury-gold)] transition-colors line-clamp-2">
|
||||
<h3 className={`text-lg font-bold ${textClasses.primary} mb-2 group-hover:text-[var(--luxury-gold)] transition-colors line-clamp-2`}>
|
||||
{relatedService.title}
|
||||
</h3>
|
||||
{relatedService.description && (
|
||||
<p className="text-gray-400 text-sm line-clamp-2 font-light">
|
||||
<p className={`${textClasses.muted} text-sm line-clamp-2 font-light`}>
|
||||
{relatedService.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -6,8 +6,29 @@ import pageContentService, { PageContent } from '../services/pageContentService'
|
||||
import serviceService, { Service } from '../../hotel_services/services/serviceService';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
// Helper function to get icon component from icon name (handles both PascalCase and lowercase)
|
||||
const getIconComponent = (iconName?: string, fallback: any = Award) => {
|
||||
if (!iconName) return fallback;
|
||||
|
||||
// Try direct match first (for PascalCase names)
|
||||
if ((LucideIcons as any)[iconName]) {
|
||||
return (LucideIcons as any)[iconName];
|
||||
}
|
||||
|
||||
// Convert to PascalCase (capitalize first letter)
|
||||
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
|
||||
if ((LucideIcons as any)[pascalCaseName]) {
|
||||
return (LucideIcons as any)[pascalCaseName];
|
||||
}
|
||||
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const ServicesPage: React.FC = () => {
|
||||
const { theme } = useTheme();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [hotelServices, setHotelServices] = useState<Service[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -156,10 +177,16 @@ const ServicesPage: React.FC = () => {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
{/* Hero Section */}
|
||||
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8 overflow-hidden relative">
|
||||
<div className={`w-full ${heroBgClasses} pt-6 sm:pt-7 md:pt-8 overflow-hidden relative`}>
|
||||
{/* Background Effects */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
||||
@@ -172,18 +199,20 @@ const ServicesPage: React.FC = () => {
|
||||
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
|
||||
<div className="relative p-2 sm:p-2.5 md:p-3 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300">
|
||||
<div className={`relative p-2 sm:p-2.5 md:p-3 ${cardClasses} rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300`}>
|
||||
<Award className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
|
||||
<span className="bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
||||
<span className={`${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||
{pageContent?.luxury_services_section_title || 'Our Services'}
|
||||
</span>
|
||||
</h1>
|
||||
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
|
||||
<p className="text-sm sm:text-base md:text-lg text-gray-300 font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4">
|
||||
<p className={`text-sm sm:text-base md:text-lg ${textClasses.secondary} font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4`}>
|
||||
{pageContent?.luxury_services_section_subtitle || 'Discover our premium services designed to enhance your stay'}
|
||||
</p>
|
||||
<div className="mt-4 flex items-center justify-center gap-2 text-[var(--luxury-gold)]/60">
|
||||
@@ -211,7 +240,7 @@ const ServicesPage: React.FC = () => {
|
||||
placeholder="Search services..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-14 pr-5 py-4 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] border border-[var(--luxury-gold)]/20 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light"
|
||||
className={`w-full pl-14 pr-5 py-4 ${inputClasses} border border-[var(--luxury-gold)]/20 rounded-xl placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,13 +250,13 @@ const ServicesPage: React.FC = () => {
|
||||
{/* Categories Filter - Top Center */}
|
||||
{allCategories.length > 0 && (
|
||||
<div className="mb-12 flex justify-center">
|
||||
<div className="inline-flex flex-wrap items-center gap-3 bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-2xl border-2 border-[var(--luxury-gold)]/20 p-4 backdrop-blur-xl shadow-2xl">
|
||||
<div className={`inline-flex flex-wrap items-center gap-3 ${cardClasses} rounded-2xl border-2 border-[var(--luxury-gold)]/20 p-4 backdrop-blur-xl shadow-2xl`}>
|
||||
<button
|
||||
onClick={() => setSelectedCategory(null)}
|
||||
className={`group relative px-6 py-3 rounded-xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
||||
selectedCategory === null
|
||||
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
||||
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)]'
|
||||
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
|
||||
}`}
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
@@ -242,7 +271,7 @@ const ServicesPage: React.FC = () => {
|
||||
className={`group relative px-6 py-3 rounded-xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
||||
selectedCategory === category
|
||||
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
||||
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)]'
|
||||
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
|
||||
}`}
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
@@ -259,7 +288,7 @@ const ServicesPage: React.FC = () => {
|
||||
<div className="flex flex-col items-center">
|
||||
{/* Results Count */}
|
||||
{!loading && filteredServices.length > 0 && (
|
||||
<div className="mb-8 text-gray-400 font-light text-sm text-center">
|
||||
<div className={`mb-8 ${textClasses.muted} font-light text-sm text-center`}>
|
||||
Showing {filteredServices.length} of {allServices.length} services
|
||||
</div>
|
||||
)}
|
||||
@@ -270,8 +299,8 @@ const ServicesPage: React.FC = () => {
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--luxury-gold)]/10 mb-6">
|
||||
<Award className="w-10 h-10 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<p className="text-gray-400 text-xl font-light">No services found</p>
|
||||
<p className="text-gray-500 text-sm mt-2">Try adjusting your search or filters</p>
|
||||
<p className={`${textClasses.muted} text-xl font-light`}>No services found</p>
|
||||
<p className={`${textClasses.muted} text-sm mt-2`}>Try adjusting your search or filters</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -288,7 +317,7 @@ const ServicesPage: React.FC = () => {
|
||||
<Link
|
||||
key={service.id}
|
||||
to={`/services/${serviceSlug}`}
|
||||
className="group relative bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3 block"
|
||||
className={`group relative ${cardClasses} rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3 block`}
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
{/* Premium Glow Effects */}
|
||||
@@ -320,7 +349,7 @@ const ServicesPage: React.FC = () => {
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-48 sm:h-56 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] flex items-center justify-center p-8">
|
||||
<div className={`h-48 sm:h-56 ${cardClasses} flex items-center justify-center p-8`}>
|
||||
{service.icon && (LucideIcons as any)[service.icon] ? (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div>
|
||||
@@ -331,7 +360,12 @@ const ServicesPage: React.FC = () => {
|
||||
) : (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div>
|
||||
<Award className="w-16 h-16 sm:w-20 sm:h-20 text-[var(--luxury-gold)] relative z-10 drop-shadow-lg" />
|
||||
{React.createElement(
|
||||
getIconComponent(pageContent?.services_fallback_icon, Award),
|
||||
{
|
||||
className: 'w-16 h-16 sm:w-20 sm:h-20 text-[var(--luxury-gold)] relative z-10 drop-shadow-lg'
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -345,11 +379,11 @@ const ServicesPage: React.FC = () => {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-2xl sm:text-3xl font-serif font-bold text-white mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight">
|
||||
<h2 className={`text-2xl sm:text-3xl font-serif font-bold ${textClasses.primary} mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight`}>
|
||||
{service.title}
|
||||
</h2>
|
||||
{service.description && (
|
||||
<p className="text-gray-300 text-base mb-6 line-clamp-3 font-light leading-relaxed">
|
||||
<p className={`${textClasses.secondary} text-base mb-6 line-clamp-3 font-light leading-relaxed`}>
|
||||
{service.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -360,7 +394,7 @@ const ServicesPage: React.FC = () => {
|
||||
{formatCurrency(service.price)}
|
||||
</span>
|
||||
{service.unit && (
|
||||
<span className="text-sm text-gray-400 font-light">
|
||||
<span className={`text-sm ${textClasses.muted} font-light`}>
|
||||
/ {service.unit}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -4,13 +4,20 @@ import { Link } from 'react-router-dom';
|
||||
import pageContentService from '../services/pageContentService';
|
||||
import type { PageContent } from '../services/pageContentService';
|
||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import Loading from '../../../shared/components/Loading';
|
||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
const TermsPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const { theme } = useTheme();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
@@ -28,24 +35,26 @@ const TermsPage: React.FC = () => {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = sanitizedContent;
|
||||
|
||||
// Add color styles to elements that don't have them
|
||||
// Get theme-aware colors
|
||||
const isLightMode = theme.theme_layout_mode === 'light';
|
||||
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||
|
||||
// Override inline colors to use theme-aware colors
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
// Only add color if not already set
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = headingColor;
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = accentColor;
|
||||
} else {
|
||||
htmlEl.style.color = bodyColor;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,7 +97,7 @@ const TermsPage: React.FC = () => {
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -100,8 +109,8 @@ const TermsPage: React.FC = () => {
|
||||
>
|
||||
<div className="text-center">
|
||||
<Scale className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Terms & Conditions</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Terms & Conditions</h1>
|
||||
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||
@@ -115,7 +124,7 @@ const TermsPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -140,32 +149,34 @@ const TermsPage: React.FC = () => {
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||
<Scale className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
||||
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||
{pageContent.title || 'Terms & Conditions'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||
prose-headings:font-elegant prose-headings:font-semibold
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:font-light prose-ul:my-4
|
||||
prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -173,7 +184,7 @@ const TermsPage: React.FC = () => {
|
||||
{/* Footer Note */}
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||
For questions about these terms, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||
{settings.company_email}
|
||||
|
||||
@@ -21,6 +21,18 @@ export interface PageContent {
|
||||
email?: string;
|
||||
address?: string;
|
||||
};
|
||||
contact_icons?: {
|
||||
hero?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
location?: string;
|
||||
name_field?: string;
|
||||
email_field?: string;
|
||||
phone_field?: string;
|
||||
subject_field?: string;
|
||||
message_field?: string;
|
||||
submit_button?: string;
|
||||
};
|
||||
map_url?: string;
|
||||
social_links?: {
|
||||
facebook?: string;
|
||||
@@ -46,6 +58,13 @@ export interface PageContent {
|
||||
features_section_title?: string;
|
||||
features_section_subtitle?: string;
|
||||
about_hero_image?: string;
|
||||
about_hero_icon?: string;
|
||||
about_contact_icons?: {
|
||||
location?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
};
|
||||
about_learn_more_icon?: string;
|
||||
mission?: string;
|
||||
vision?: string;
|
||||
team?: Array<{ name: string; role: string; image?: string; bio?: string; social_links?: { linkedin?: string; twitter?: string; email?: string } }>;
|
||||
@@ -88,6 +107,7 @@ export interface PageContent {
|
||||
services_section_button_text?: string;
|
||||
services_section_button_link?: string;
|
||||
services_section_limit?: number;
|
||||
services_fallback_icon?: string;
|
||||
luxury_services?: Array<{
|
||||
icon?: string;
|
||||
title: string;
|
||||
@@ -204,6 +224,18 @@ export interface UpdatePageContentData {
|
||||
email?: string;
|
||||
address?: string;
|
||||
};
|
||||
contact_icons?: {
|
||||
hero?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
location?: string;
|
||||
name_field?: string;
|
||||
email_field?: string;
|
||||
phone_field?: string;
|
||||
subject_field?: string;
|
||||
message_field?: string;
|
||||
submit_button?: string;
|
||||
};
|
||||
map_url?: string;
|
||||
social_links?: {
|
||||
facebook?: string;
|
||||
@@ -229,6 +261,13 @@ export interface UpdatePageContentData {
|
||||
features_section_title?: string;
|
||||
features_section_subtitle?: string;
|
||||
about_hero_image?: string;
|
||||
about_hero_icon?: string;
|
||||
about_contact_icons?: {
|
||||
location?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
};
|
||||
about_learn_more_icon?: string;
|
||||
mission?: string;
|
||||
vision?: string;
|
||||
team?: Array<{ name: string; role: string; image?: string; bio?: string; social_links?: { linkedin?: string; twitter?: string; email?: string } }>;
|
||||
@@ -271,6 +310,7 @@ export interface UpdatePageContentData {
|
||||
services_section_button_text?: string;
|
||||
services_section_button_link?: string;
|
||||
services_section_limit?: number;
|
||||
services_fallback_icon?: string;
|
||||
luxury_services?: Array<{
|
||||
icon?: string;
|
||||
title: string;
|
||||
|
||||
@@ -6,6 +6,7 @@ import useAuthStore from '../../../store/useAuthStore';
|
||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||
import { toast } from 'react-toastify';
|
||||
import ConfirmationDialog from '../../../shared/components/ConfirmationDialog';
|
||||
import { formatWorkingHours } from '../../../shared/utils/format';
|
||||
|
||||
interface ChatWidgetProps {
|
||||
onClose?: () => void;
|
||||
@@ -405,7 +406,9 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
|
||||
{!isWithinBusinessHours ? (
|
||||
<p className="text-xs text-slate-700/80 font-light flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
Chat available 9 AM - 5 PM
|
||||
Chat available {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined
|
||||
? formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)
|
||||
: '9:00 AM - 5:00 PM'}
|
||||
</p>
|
||||
) : chat?.status === 'pending' ? (
|
||||
<p className="text-xs text-slate-700/80 font-light">Waiting for staff...</p>
|
||||
@@ -459,7 +462,9 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-900 mb-1">Chat Hours</p>
|
||||
<p className="text-xs text-slate-600 font-light">
|
||||
Our chat support is available Monday to Friday, {settings.chat_working_hours_start || 9}:00 AM - {settings.chat_working_hours_end || 17}:00 PM.
|
||||
Our chat support is available Monday to Friday, {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined
|
||||
? formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)
|
||||
: '9:00 AM - 5:00 PM'}.
|
||||
Please leave your inquiry below and we'll get back to you as soon as possible.
|
||||
</p>
|
||||
</div>
|
||||
@@ -585,6 +590,18 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
|
||||
{}
|
||||
{!showVisitorForm && isWithinBusinessHours && (
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-slate-50/50 to-white">
|
||||
{/* Working Hours Info Banner */}
|
||||
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
|
||||
<div className="mb-4 p-3 bg-gradient-to-r from-[var(--luxury-gold)]/5 to-[var(--luxury-gold-dark)]/5 rounded-lg border border-[var(--luxury-gold)]/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 text-[var(--luxury-gold)] flex-shrink-0" />
|
||||
<p className="text-xs text-slate-600 font-light">
|
||||
<span className="font-medium text-slate-700">Chat Support Hours:</span>{' '}
|
||||
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loading && !chat ? (
|
||||
<div className="text-center text-slate-500 py-8 font-light">
|
||||
Starting chat...
|
||||
|
||||
@@ -30,14 +30,13 @@ interface CurrencyProviderProps {
|
||||
}
|
||||
|
||||
export const CurrencyProvider: React.FC<CurrencyProviderProps> = ({ children }) => {
|
||||
const [currency, setCurrencyState] = useState<string>(CURRENCY.VND);
|
||||
const [currency, setCurrencyState] = useState<string>(CURRENCY.USD);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
const supportedCurrencies = [
|
||||
CURRENCY.VND,
|
||||
CURRENCY.USD,
|
||||
CURRENCY.EUR,
|
||||
'GBP',
|
||||
CURRENCY.GBP,
|
||||
'JPY',
|
||||
'CNY',
|
||||
'KRW',
|
||||
@@ -59,14 +58,14 @@ export const CurrencyProvider: React.FC<CurrencyProviderProps> = ({ children })
|
||||
localStorage.setItem('currency', platformCurrency);
|
||||
} else {
|
||||
|
||||
setCurrencyState(CURRENCY.VND);
|
||||
localStorage.setItem('currency', CURRENCY.VND);
|
||||
setCurrencyState(CURRENCY.USD);
|
||||
localStorage.setItem('currency', CURRENCY.USD);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading platform currency:', error);
|
||||
|
||||
setCurrencyState(CURRENCY.VND);
|
||||
localStorage.setItem('currency', CURRENCY.VND);
|
||||
setCurrencyState(CURRENCY.USD);
|
||||
localStorage.setItem('currency', CURRENCY.USD);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -81,7 +80,7 @@ export const CurrencyProvider: React.FC<CurrencyProviderProps> = ({ children })
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('currencyChanged', {
|
||||
detail: { currency: localStorage.getItem('currency') || CURRENCY.VND }
|
||||
detail: { currency: localStorage.getItem('currency') || CURRENCY.USD }
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,6 +15,8 @@ import Recaptcha from '../../../shared/components/Recaptcha';
|
||||
import { recaptchaService } from '../../system/services/systemSettingsService';
|
||||
import { useAntibotForm } from '../../auth/hooks/useAntibotForm';
|
||||
import HoneypotField from '../../../shared/components/HoneypotField';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import { getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
interface ReviewSectionProps {
|
||||
roomId: number;
|
||||
@@ -41,6 +43,10 @@ type ReviewFormData = {
|
||||
const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
roomId
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
const { openModal } = useAuthModal();
|
||||
const [reviews, setReviews] = useState<Review[]>([]);
|
||||
@@ -184,8 +190,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{}
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20 p-3 sm:p-4 backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5">
|
||||
<h3 className="text-sm sm:text-base font-serif font-semibold text-white mb-3 tracking-wide">
|
||||
<div className={`${cardClasses} rounded-lg border border-[var(--luxury-gold)]/20 p-3 sm:p-4 backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5`}>
|
||||
<h3 className={`text-sm sm:text-base font-serif font-semibold ${textClasses.primary} mb-3 tracking-wide`}>
|
||||
Customer Reviews
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -203,7 +209,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[10px] sm:text-xs text-gray-400 mt-1.5 font-light">
|
||||
<div className={`text-[10px] sm:text-xs ${textClasses.muted} mt-1.5 font-light`}>
|
||||
{totalReviews > 0
|
||||
? `${totalReviews} review${totalReviews !== 1 ? 's' : ''}`
|
||||
: 'No reviews yet'}
|
||||
@@ -214,8 +220,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
|
||||
{}
|
||||
{isAuthenticated ? (
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20 p-3 sm:p-4 backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5">
|
||||
<h4 className="text-xs sm:text-sm font-serif font-semibold text-white mb-3 tracking-wide">
|
||||
<div className={`${cardClasses} rounded-lg border border-[var(--luxury-gold)]/20 p-3 sm:p-4 backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5`}>
|
||||
<h4 className={`text-xs sm:text-sm font-serif font-semibold ${textClasses.primary} mb-3 tracking-wide`}>
|
||||
Write Your Review
|
||||
</h4>
|
||||
<form onSubmit={handleSubmit(onSubmit)}
|
||||
@@ -230,8 +236,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-[10px] sm:text-xs font-light
|
||||
text-gray-300 mb-1.5 tracking-wide"
|
||||
<label className={`block text-[10px] sm:text-xs font-light
|
||||
${textClasses.secondary} mb-1.5 tracking-wide`}
|
||||
>
|
||||
Your Rating
|
||||
</label>
|
||||
@@ -253,8 +259,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
<div>
|
||||
<label
|
||||
htmlFor="comment"
|
||||
className="block text-[10px] sm:text-xs font-light
|
||||
text-gray-300 mb-1.5 tracking-wide"
|
||||
className={`block text-[10px] sm:text-xs font-light
|
||||
${textClasses.secondary} mb-1.5 tracking-wide`}
|
||||
>
|
||||
Comment
|
||||
</label>
|
||||
@@ -262,11 +268,11 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
{...register('comment')}
|
||||
id="comment"
|
||||
rows={3}
|
||||
className="w-full px-2.5 py-1.5 bg-[#0a0a0a] border
|
||||
border-[var(--luxury-gold)]/20 rounded-lg text-white placeholder-gray-500 text-xs sm:text-sm
|
||||
className={`w-full px-2.5 py-1.5 ${inputClasses} border
|
||||
border-[var(--luxury-gold)]/20 rounded-lg placeholder-gray-500 text-xs sm:text-sm
|
||||
focus:ring-2 focus:ring-[var(--luxury-gold)]/50
|
||||
focus:border-[var(--luxury-gold)] transition-all duration-300
|
||||
font-light tracking-wide resize-none"
|
||||
font-light tracking-wide resize-none`}
|
||||
placeholder="Share your experience..."
|
||||
/>
|
||||
{errors.comment && (
|
||||
@@ -284,7 +290,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
console.error('reCAPTCHA error:', error);
|
||||
setRecaptchaToken(null);
|
||||
}}
|
||||
theme="dark"
|
||||
theme={theme.theme_layout_mode === 'light' ? 'light' : 'dark'}
|
||||
size="normal"
|
||||
/>
|
||||
</div>
|
||||
@@ -323,7 +329,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
|
||||
{}
|
||||
<div>
|
||||
<h4 className="text-xs sm:text-sm font-serif font-semibold text-white mb-3 tracking-wide">
|
||||
<h4 className={`text-xs sm:text-sm font-serif font-semibold ${textClasses.primary} mb-3 tracking-wide`}>
|
||||
All Reviews ({reviews.length > 0 ? reviews.length : totalReviews})
|
||||
</h4>
|
||||
|
||||
@@ -332,25 +338,25 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20 p-3
|
||||
animate-pulse"
|
||||
className={`${cardClasses} rounded-lg border border-[var(--luxury-gold)]/20 p-3
|
||||
animate-pulse`}
|
||||
>
|
||||
<div className="h-3 bg-gray-700
|
||||
rounded w-1/4 mb-2"
|
||||
<div className={`h-3 ${theme.theme_layout_mode === 'light' ? 'bg-gray-300' : 'bg-gray-700'}
|
||||
rounded w-1/4 mb-2`}
|
||||
/>
|
||||
<div className="h-3 bg-gray-700
|
||||
rounded w-3/4"
|
||||
<div className={`h-3 ${theme.theme_layout_mode === 'light' ? 'bg-gray-300' : 'bg-gray-700'}
|
||||
rounded w-3/4`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : reviews.length === 0 ? (
|
||||
<div className="text-center py-6 sm:py-8 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20 p-4"
|
||||
<div className={`text-center py-6 sm:py-8 ${cardClasses} rounded-lg border border-[var(--luxury-gold)]/20 p-4`}
|
||||
>
|
||||
<p className="text-gray-300 text-sm sm:text-base font-light">
|
||||
<p className={`${textClasses.secondary} text-sm sm:text-base font-light`}>
|
||||
No reviews yet
|
||||
</p>
|
||||
<p className="text-gray-400 text-xs sm:text-sm mt-1.5 font-light">
|
||||
<p className={`${textClasses.muted} text-xs sm:text-sm mt-1.5 font-light`}>
|
||||
Be the first to review this room!
|
||||
</p>
|
||||
</div>
|
||||
@@ -359,15 +365,15 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
{reviews.map((review) => (
|
||||
<div
|
||||
key={review.id}
|
||||
className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20
|
||||
p-3 sm:p-4 backdrop-blur-xl shadow-sm shadow-[var(--luxury-gold)]/5"
|
||||
className={`${cardClasses} rounded-lg border border-[var(--luxury-gold)]/20
|
||||
p-3 sm:p-4 backdrop-blur-xl shadow-sm shadow-[var(--luxury-gold)]/5`}
|
||||
>
|
||||
<div className="flex items-start
|
||||
justify-between mb-2"
|
||||
>
|
||||
<div>
|
||||
<h5 className="font-semibold
|
||||
text-white text-xs sm:text-sm"
|
||||
<h5 className={`font-semibold
|
||||
${textClasses.primary} text-xs sm:text-sm`}
|
||||
>
|
||||
{review.user?.full_name || 'Guest'}
|
||||
</h5>
|
||||
@@ -378,15 +384,15 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||
rating={review.rating}
|
||||
size="sm"
|
||||
/>
|
||||
<span className="text-[10px] sm:text-xs
|
||||
text-gray-400 font-light"
|
||||
<span className={`text-[10px] sm:text-xs
|
||||
${textClasses.muted} font-light`}
|
||||
>
|
||||
{formatDate(review.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-300 leading-relaxed text-xs sm:text-sm font-light">
|
||||
<p className={`${textClasses.secondary} leading-relaxed text-xs sm:text-sm font-light`}>
|
||||
{review.comment}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'react-datepicker/dist/react-datepicker.css';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
||||
import { Calendar, DollarSign, Users, X } from 'lucide-react';
|
||||
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||
import { getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
|
||||
|
||||
interface RoomFilterProps {
|
||||
onFilterChange?: (filters: FilterValues) => void;
|
||||
@@ -19,6 +21,10 @@ export interface FilterValues {
|
||||
}
|
||||
|
||||
const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
const { theme } = useTheme();
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { formatCurrency: formatCurrencyUtil } = useFormatCurrency();
|
||||
|
||||
@@ -249,10 +255,10 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
||||
<div className={`${cardClasses}
|
||||
rounded-xl border border-[var(--luxury-gold)]/30
|
||||
backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/10
|
||||
p-4 sm:p-5 md:p-6"
|
||||
p-4 sm:p-5 md:p-6`}
|
||||
>
|
||||
<div className="flex items-center gap-2 sm:gap-3 mb-4 sm:mb-5 md:mb-6">
|
||||
<div className="p-1.5 sm:p-2 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||
@@ -261,7 +267,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-lg sm:text-xl font-serif font-semibold mb-0 text-white tracking-tight">
|
||||
<h2 className={`text-lg sm:text-xl font-serif font-semibold mb-0 ${textClasses.primary} tracking-tight`}>
|
||||
Room Filters
|
||||
</h2>
|
||||
</div>
|
||||
@@ -270,8 +276,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="type"
|
||||
className="block text-sm font-medium
|
||||
text-gray-200 mb-2 tracking-wide"
|
||||
className={`block text-sm font-medium
|
||||
${textClasses.secondary} mb-2 tracking-wide`}
|
||||
>
|
||||
Room Type
|
||||
</label>
|
||||
@@ -281,20 +287,20 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
name="type"
|
||||
value={filters.type || ''}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-4 py-3.5 bg-[#1a1a1a] border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
||||
className={`w-full px-4 py-3.5 ${inputClasses} border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||
placeholder-gray-400 focus:ring-2
|
||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||
transition-all duration-300 font-normal tracking-wide
|
||||
appearance-none cursor-pointer
|
||||
hover:border-[var(--luxury-gold)]/50"
|
||||
hover:border-[var(--luxury-gold)]/50`}
|
||||
>
|
||||
<option value="" className="bg-[#1a1a1a] text-white">All Room Types</option>
|
||||
<option value="Standard Room" className="bg-[#1a1a1a] text-white">Standard Room</option>
|
||||
<option value="Deluxe Room" className="bg-[#1a1a1a] text-white">Deluxe Room</option>
|
||||
<option value="Luxury Room" className="bg-[#1a1a1a] text-white">Luxury Room</option>
|
||||
<option value="Family Room" className="bg-[#1a1a1a] text-white">Family Room</option>
|
||||
<option value="Twin Room" className="bg-[#1a1a1a] text-white">Twin Room</option>
|
||||
<option value="" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>All Room Types</option>
|
||||
<option value="Standard Room" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>Standard Room</option>
|
||||
<option value="Deluxe Room" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>Deluxe Room</option>
|
||||
<option value="Luxury Room" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>Luxury Room</option>
|
||||
<option value="Family Room" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>Family Room</option>
|
||||
<option value="Twin Room" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>Twin Room</option>
|
||||
</select>
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||
<svg className="w-5 h-5 text-[var(--luxury-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -309,7 +315,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="from"
|
||||
className="block text-sm font-medium text-gray-200 mb-2 tracking-wide flex items-center gap-2"
|
||||
className={`block text-sm font-medium ${textClasses.secondary} mb-2 tracking-wide flex items-center gap-2`}
|
||||
>
|
||||
<Calendar className="w-4 h-4 text-[var(--luxury-gold)]" />
|
||||
Check-in Date
|
||||
@@ -330,12 +336,12 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
minDate={new Date()}
|
||||
dateFormat="dd/MM/yyyy"
|
||||
placeholderText="Select check-in"
|
||||
className="w-full px-4 py-3.5 pl-11 bg-[#1a1a1a] border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
||||
className={`w-full px-4 py-3.5 pl-11 ${inputClasses} border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||
placeholder-gray-400 focus:ring-2
|
||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||
transition-all duration-300 font-normal tracking-wide
|
||||
hover:border-[var(--luxury-gold)]/50"
|
||||
hover:border-[var(--luxury-gold)]/50`}
|
||||
wrapperClassName="w-full"
|
||||
/>
|
||||
{checkInDate && (
|
||||
@@ -348,8 +354,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
setCheckOutDate(null);
|
||||
}
|
||||
}}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2
|
||||
text-gray-400 hover:text-[var(--luxury-gold)] transition-colors"
|
||||
className={`absolute right-3 top-1/2 -translate-y-1/2
|
||||
${textClasses.muted} hover:text-[var(--luxury-gold)] transition-colors`}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -362,7 +368,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="to"
|
||||
className="block text-sm font-medium text-gray-200 mb-2 tracking-wide flex items-center gap-2"
|
||||
className={`block text-sm font-medium ${textClasses.secondary} mb-2 tracking-wide flex items-center gap-2`}
|
||||
>
|
||||
<Calendar className="w-4 h-4 text-[var(--luxury-gold)]" />
|
||||
Check-out Date
|
||||
@@ -382,13 +388,13 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
disabled={!checkInDate}
|
||||
dateFormat="dd/MM/yyyy"
|
||||
placeholderText={checkInDate ? "Select check-out" : "Select check-in first"}
|
||||
className="w-full px-4 py-3.5 pl-11 bg-[#1a1a1a] border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
||||
className={`w-full px-4 py-3.5 pl-11 ${inputClasses} border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||
placeholder-gray-400 focus:ring-2
|
||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||
transition-all duration-300 font-normal tracking-wide
|
||||
hover:border-[var(--luxury-gold)]/50
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
wrapperClassName="w-full"
|
||||
/>
|
||||
{checkOutDate && (
|
||||
@@ -398,8 +404,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
e.stopPropagation();
|
||||
setCheckOutDate(null);
|
||||
}}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2
|
||||
text-gray-400 hover:text-[var(--luxury-gold)] transition-colors"
|
||||
className={`absolute right-3 top-1/2 -translate-y-1/2
|
||||
${textClasses.muted} hover:text-[var(--luxury-gold)] transition-colors`}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -408,7 +414,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
w-5 h-5 text-[var(--luxury-gold)] pointer-events-none" />
|
||||
</div>
|
||||
{checkInDate && !checkOutDate && (
|
||||
<p className="mt-1.5 text-xs text-gray-400 font-light">
|
||||
<p className={`mt-1.5 text-xs ${textClasses.muted} font-light`}>
|
||||
Select check-out date (minimum 1 night stay)
|
||||
</p>
|
||||
)}
|
||||
@@ -417,7 +423,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
|
||||
{}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-200 mb-3 tracking-wide flex items-center gap-2">
|
||||
<label className={`block text-sm font-medium ${textClasses.secondary} mb-3 tracking-wide flex items-center gap-2`}>
|
||||
<DollarSign className="w-4 h-4 text-[var(--luxury-gold)]" />
|
||||
Price Range
|
||||
</label>
|
||||
@@ -425,8 +431,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="minPrice"
|
||||
className="block text-xs font-normal
|
||||
text-gray-400 mb-1.5 tracking-wide"
|
||||
className={`block text-xs font-normal
|
||||
${textClasses.muted} mb-1.5 tracking-wide`}
|
||||
>
|
||||
Minimum
|
||||
</label>
|
||||
@@ -446,20 +452,20 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
placeholder="0"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9.]*"
|
||||
className="w-full px-4 py-3.5 pl-10 bg-[#1a1a1a] border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
||||
className={`w-full px-4 py-3.5 pl-10 ${inputClasses} border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||
placeholder-gray-400 focus:ring-2
|
||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||
transition-all duration-300 font-normal tracking-wide
|
||||
hover:border-[var(--luxury-gold)]/50"
|
||||
hover:border-[var(--luxury-gold)]/50`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="maxPrice"
|
||||
className="block text-xs font-normal
|
||||
text-gray-400 mb-1.5 tracking-wide"
|
||||
className={`block text-xs font-normal
|
||||
${textClasses.muted} mb-1.5 tracking-wide`}
|
||||
>
|
||||
Maximum
|
||||
</label>
|
||||
@@ -479,12 +485,12 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
placeholder="No limit"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9.]*"
|
||||
className="w-full px-4 py-3.5 pl-10 bg-[#1a1a1a] border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
||||
className={`w-full px-4 py-3.5 pl-10 ${inputClasses} border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||
placeholder-gray-400 focus:ring-2
|
||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||
transition-all duration-300 font-normal tracking-wide
|
||||
hover:border-[var(--luxury-gold)]/50"
|
||||
hover:border-[var(--luxury-gold)]/50`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -495,8 +501,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="capacity"
|
||||
className="block text-sm font-medium
|
||||
text-gray-200 mb-2 tracking-wide flex items-center gap-2"
|
||||
className={`block text-sm font-medium
|
||||
${textClasses.secondary} mb-2 tracking-wide flex items-center gap-2`}
|
||||
>
|
||||
<Users className="w-4 h-4 text-[var(--luxury-gold)]" />
|
||||
Number of Guests
|
||||
@@ -513,37 +519,37 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
placeholder="1"
|
||||
min="1"
|
||||
max="10"
|
||||
className="w-full px-4 py-3.5 pl-11 bg-[#1a1a1a] border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
||||
className={`w-full px-4 py-3.5 pl-11 ${inputClasses} border-2
|
||||
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||
placeholder-gray-400 focus:ring-2
|
||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||
transition-all duration-300 font-normal tracking-wide
|
||||
hover:border-[var(--luxury-gold)]/50
|
||||
[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none
|
||||
[&::-webkit-inner-spin-button]:appearance-none"
|
||||
[&::-webkit-inner-spin-button]:appearance-none`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-200 mb-3 tracking-wide">
|
||||
<label className={`block text-sm font-medium ${textClasses.secondary} mb-3 tracking-wide`}>
|
||||
Amenities
|
||||
</label>
|
||||
{availableAmenities.length === 0 ? (
|
||||
<div className="text-sm text-gray-400 font-light bg-[#1a1a1a]/50
|
||||
border border-[var(--luxury-gold)]/20 rounded-lg px-4 py-3">
|
||||
<div className={`text-sm ${textClasses.muted} font-light ${theme.theme_layout_mode === 'light' ? 'bg-gray-50' : 'bg-[#1a1a1a]/50'}
|
||||
border border-[var(--luxury-gold)]/20 rounded-lg px-4 py-3`}>
|
||||
Loading amenities...
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-[#1a1a1a]/50 border border-[var(--luxury-gold)]/20 rounded-lg p-3
|
||||
max-h-48 overflow-y-auto custom-scrollbar space-y-2">
|
||||
<div className={`${theme.theme_layout_mode === 'light' ? 'bg-gray-50' : 'bg-[#1a1a1a]/50'} border border-[var(--luxury-gold)]/20 rounded-lg p-3
|
||||
max-h-48 overflow-y-auto custom-scrollbar space-y-2`}>
|
||||
{availableAmenities.map((amenity) => (
|
||||
<label
|
||||
key={amenity}
|
||||
className="flex items-center gap-3 text-sm w-full font-normal tracking-wide
|
||||
className={`flex items-center gap-3 text-sm w-full font-normal tracking-wide
|
||||
hover:text-[var(--luxury-gold)] transition-colors cursor-pointer
|
||||
text-gray-200 group"
|
||||
${textClasses.secondary} group`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -584,11 +590,11 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleReset}
|
||||
className="flex-1 bg-[#0a0a0a] backdrop-blur-sm text-gray-300
|
||||
className={`flex-1 ${theme.theme_layout_mode === 'light' ? 'bg-gray-100' : 'bg-[#0a0a0a]'} backdrop-blur-sm ${textClasses.secondary}
|
||||
py-2.5 sm:py-3 px-4 rounded-sm border border-[var(--luxury-gold)]/30
|
||||
hover:bg-[#1a1a1a] hover:border-[var(--luxury-gold)] hover:text-[var(--luxury-gold)] active:scale-95
|
||||
${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-200' : 'hover:bg-[#1a1a1a]'} hover:border-[var(--luxury-gold)] hover:text-[var(--luxury-gold)] active:scale-95
|
||||
transition-all font-medium tracking-wide text-sm sm:text-base
|
||||
touch-manipulation min-h-[44px]"
|
||||
touch-manipulation min-h-[44px]`}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
|
||||
@@ -44,6 +44,8 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Normalize value - ensure it's never an empty string, use a default if needed
|
||||
const normalizedValue = value && value.trim() ? value : undefined;
|
||||
|
||||
const allIcons = useMemo(() => {
|
||||
const icons: string[] = [];
|
||||
@@ -59,25 +61,57 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
||||
'memo'
|
||||
]);
|
||||
|
||||
for (const iconName in LucideIcons) {
|
||||
|
||||
if (
|
||||
excludedNames.has(iconName) ||
|
||||
iconName.startsWith('_') ||
|
||||
iconName[0] !== iconName[0].toUpperCase()
|
||||
) {
|
||||
continue;
|
||||
try {
|
||||
// Check if LucideIcons is available
|
||||
if (!LucideIcons || typeof LucideIcons !== 'object') {
|
||||
console.error('IconPicker: LucideIcons is not available');
|
||||
// Return popular icons as fallback
|
||||
return popularIcons.filter(icon => icon);
|
||||
}
|
||||
|
||||
const iconComponent = (LucideIcons as any)[iconName];
|
||||
|
||||
if (typeof iconComponent === 'function') {
|
||||
icons.push(iconName);
|
||||
for (const iconName in LucideIcons) {
|
||||
// Skip if excluded
|
||||
if (excludedNames.has(iconName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if starts with underscore
|
||||
if (iconName.startsWith('_')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if empty or doesn't start with uppercase letter
|
||||
if (!iconName || iconName.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if first character is uppercase letter
|
||||
const firstChar = iconName.charAt(0);
|
||||
if (firstChar < 'A' || firstChar > 'Z') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const iconComponent = (LucideIcons as any)[iconName];
|
||||
|
||||
// Only include if it's a function (React component)
|
||||
if (typeof iconComponent === 'function') {
|
||||
icons.push(iconName);
|
||||
}
|
||||
}
|
||||
|
||||
// If no icons found, use popular icons as fallback
|
||||
if (icons.length === 0) {
|
||||
console.warn('IconPicker: No icons found, using popular icons as fallback');
|
||||
return popularIcons.filter(icon => icon);
|
||||
}
|
||||
|
||||
const sorted = icons.sort();
|
||||
return sorted;
|
||||
} catch (error) {
|
||||
console.error('IconPicker: Error loading icons:', error);
|
||||
// Return popular icons as fallback
|
||||
return popularIcons.filter(icon => icon);
|
||||
}
|
||||
|
||||
const sorted = icons.sort();
|
||||
return sorted;
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -93,7 +127,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
||||
);
|
||||
}, [searchQuery, allIcons]);
|
||||
|
||||
const selectedIcon = value && (LucideIcons as any)[value] ? (LucideIcons as any)[value] : null;
|
||||
const selectedIcon = normalizedValue && (LucideIcons as any)[normalizedValue] ? (LucideIcons as any)[normalizedValue] : null;
|
||||
|
||||
const handleIconSelect = (iconName: string) => {
|
||||
onChange(iconName);
|
||||
@@ -113,7 +147,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
||||
{selectedIcon ? (
|
||||
<>
|
||||
{React.createElement(selectedIcon, { className: 'w-5 h-5 text-gray-700' })}
|
||||
<span className="text-gray-700 font-medium">{value}</span>
|
||||
<span className="text-gray-700 font-medium">{normalizedValue}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-gray-400">Select an icon</span>
|
||||
@@ -152,7 +186,12 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto p-4">
|
||||
{filteredIcons.length > 0 ? (
|
||||
{allIcons.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p className="text-sm">Unable to load icons. Please refresh the page.</p>
|
||||
<p className="text-xs mt-2">If the problem persists, check the browser console for errors.</p>
|
||||
</div>
|
||||
) : filteredIcons.length > 0 ? (
|
||||
<>
|
||||
{!searchQuery.trim() && (
|
||||
<div className="mb-3">
|
||||
@@ -166,7 +205,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
||||
const IconComponent = (LucideIcons as any)[iconName];
|
||||
if (!IconComponent) return null;
|
||||
|
||||
const isSelected = value === iconName;
|
||||
const isSelected = normalizedValue === iconName;
|
||||
const isPopular = !searchQuery.trim() && popularIcons.includes(iconName);
|
||||
|
||||
try {
|
||||
@@ -208,8 +247,14 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>No icons found matching "{searchQuery}"</p>
|
||||
<p className="text-xs mt-2">Try a different search term</p>
|
||||
{searchQuery.trim() ? (
|
||||
<>
|
||||
<p>No icons found matching "{searchQuery}"</p>
|
||||
<p className="text-xs mt-2">Try a different search term</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm">No icons available</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -139,6 +139,10 @@ export interface CompanySettingsResponse {
|
||||
tax_rate: number;
|
||||
chat_working_hours_start: number;
|
||||
chat_working_hours_end: number;
|
||||
bank_name?: string;
|
||||
bank_account_number?: string;
|
||||
bank_account_holder?: string;
|
||||
bank_code?: string;
|
||||
updated_at?: string | null;
|
||||
updated_by?: string | null;
|
||||
};
|
||||
@@ -154,6 +158,10 @@ export interface UpdateCompanySettingsRequest {
|
||||
tax_rate?: number;
|
||||
chat_working_hours_start?: number;
|
||||
chat_working_hours_end?: number;
|
||||
bank_name?: string;
|
||||
bank_account_number?: string;
|
||||
bank_account_holder?: string;
|
||||
bank_code?: string;
|
||||
}
|
||||
|
||||
export interface UploadLogoResponse {
|
||||
@@ -219,6 +227,7 @@ export interface ThemeSettingsResponse {
|
||||
theme_primary_light: string;
|
||||
theme_primary_dark: string;
|
||||
theme_primary_accent: string;
|
||||
theme_layout_mode: string; // 'dark' or 'light'
|
||||
updated_at?: string | null;
|
||||
updated_by?: string | null;
|
||||
};
|
||||
@@ -230,6 +239,7 @@ export interface UpdateThemeSettingsRequest {
|
||||
theme_primary_light?: string;
|
||||
theme_primary_dark?: string;
|
||||
theme_primary_accent?: string;
|
||||
theme_layout_mode?: string; // 'dark' or 'light'
|
||||
}
|
||||
|
||||
export interface VerifyRecaptchaRequest {
|
||||
|
||||
@@ -14,7 +14,6 @@ const CurrencySettingsPage: React.FC = () => {
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const currencyNames: Record<string, string> = {
|
||||
VND: 'Vietnamese Dong',
|
||||
USD: 'US Dollar',
|
||||
EUR: 'Euro',
|
||||
GBP: 'British Pound',
|
||||
|
||||
@@ -250,6 +250,7 @@ const PageContentDashboard: React.FC = () => {
|
||||
services_section_button_text: contents.home.services_section_button_text || '',
|
||||
services_section_button_link: contents.home.services_section_button_link || '',
|
||||
services_section_limit: contents.home.services_section_limit || 6,
|
||||
services_fallback_icon: contents.home.services_fallback_icon || 'Award',
|
||||
luxury_experiences_section_title: contents.home.luxury_experiences_section_title || '',
|
||||
luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '',
|
||||
luxury_experiences: normalizeArray(contents.home.luxury_experiences),
|
||||
@@ -284,19 +285,56 @@ const PageContentDashboard: React.FC = () => {
|
||||
|
||||
// Contact
|
||||
if (contents.contact) {
|
||||
const defaultContactIcons = {
|
||||
hero: 'Mail',
|
||||
email: 'Mail',
|
||||
phone: 'Phone',
|
||||
location: 'MapPin',
|
||||
name_field: 'User',
|
||||
email_field: 'Mail',
|
||||
phone_field: 'Phone',
|
||||
subject_field: 'MessageSquare',
|
||||
message_field: 'MessageSquare',
|
||||
submit_button: 'Send',
|
||||
};
|
||||
setContactData({
|
||||
title: contents.contact.title || '',
|
||||
subtitle: contents.contact.subtitle || '',
|
||||
description: contents.contact.description || '',
|
||||
content: contents.contact.content || '',
|
||||
map_url: contents.contact.map_url || '',
|
||||
contact_icons: contents.contact.contact_icons ? {
|
||||
...defaultContactIcons,
|
||||
...contents.contact.contact_icons,
|
||||
} : defaultContactIcons,
|
||||
meta_title: contents.contact.meta_title || '',
|
||||
meta_description: contents.contact.meta_description || '',
|
||||
});
|
||||
} else {
|
||||
// Initialize with defaults even if no contact data exists
|
||||
setContactData({
|
||||
contact_icons: {
|
||||
hero: 'Mail',
|
||||
email: 'Mail',
|
||||
phone: 'Phone',
|
||||
location: 'MapPin',
|
||||
name_field: 'User',
|
||||
email_field: 'Mail',
|
||||
phone_field: 'Phone',
|
||||
subject_field: 'MessageSquare',
|
||||
message_field: 'MessageSquare',
|
||||
submit_button: 'Send',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// About
|
||||
if (contents.about) {
|
||||
const defaultAboutContactIcons = {
|
||||
location: 'MapPin',
|
||||
phone: 'Phone',
|
||||
email: 'Mail',
|
||||
};
|
||||
setAboutData({
|
||||
title: contents.about.title || '',
|
||||
subtitle: contents.about.subtitle || '',
|
||||
@@ -306,6 +344,12 @@ const PageContentDashboard: React.FC = () => {
|
||||
values: normalizeArray(contents.about.values),
|
||||
features: normalizeArray(contents.about.features),
|
||||
about_hero_image: contents.about.about_hero_image || '',
|
||||
about_hero_icon: contents.about.about_hero_icon || 'Hotel',
|
||||
about_contact_icons: contents.about.about_contact_icons ? {
|
||||
...defaultAboutContactIcons,
|
||||
...contents.about.about_contact_icons,
|
||||
} : defaultAboutContactIcons,
|
||||
about_learn_more_icon: contents.about.about_learn_more_icon || 'Hotel',
|
||||
mission: contents.about.mission || '',
|
||||
vision: contents.about.vision || '',
|
||||
team: normalizeArray(contents.about.team),
|
||||
@@ -314,6 +358,17 @@ const PageContentDashboard: React.FC = () => {
|
||||
meta_title: contents.about.meta_title || '',
|
||||
meta_description: contents.about.meta_description || '',
|
||||
});
|
||||
} else {
|
||||
// Initialize with defaults even if no about data exists
|
||||
setAboutData({
|
||||
about_hero_icon: 'Hotel',
|
||||
about_contact_icons: {
|
||||
location: 'MapPin',
|
||||
phone: 'Phone',
|
||||
email: 'Mail',
|
||||
},
|
||||
about_learn_more_icon: 'Hotel',
|
||||
});
|
||||
}
|
||||
|
||||
// Footer
|
||||
@@ -1354,7 +1409,7 @@ const PageContentDashboard: React.FC = () => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<IconPicker
|
||||
value={amenity?.icon || ''}
|
||||
value={amenity?.icon && amenity.icon.trim() ? amenity.icon : 'Sparkles'}
|
||||
onChange={(iconName) => {
|
||||
setHomeData((prevData) => {
|
||||
const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
|
||||
@@ -1550,7 +1605,7 @@ const PageContentDashboard: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<IconPicker
|
||||
value={feature?.icon || ''}
|
||||
value={feature?.icon && feature.icon.trim() ? feature.icon : 'Star'}
|
||||
onChange={(iconName) => {
|
||||
setHomeData((prevData) => {
|
||||
const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
|
||||
@@ -2031,7 +2086,7 @@ const PageContentDashboard: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<IconPicker
|
||||
value={stat?.icon || ''}
|
||||
value={stat?.icon && stat.icon.trim() ? stat.icon : 'TrendingUp'}
|
||||
onChange={(iconName) => {
|
||||
const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
|
||||
currentStats[index] = { ...currentStats[index], icon: iconName };
|
||||
@@ -2177,7 +2232,7 @@ const PageContentDashboard: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<IconPicker
|
||||
value={feature?.icon || ''}
|
||||
value={feature?.icon && feature.icon.trim() ? feature.icon : 'Star'}
|
||||
onChange={(iconName) => {
|
||||
setHomeData((prevData) => {
|
||||
const currentFeatures = Array.isArray(prevData.features) ? [...prevData.features] : [];
|
||||
@@ -2303,6 +2358,14 @@ const PageContentDashboard: React.FC = () => {
|
||||
placeholder="6"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Services Fallback Icon</label>
|
||||
<p className="text-xs text-gray-500 mb-2">Icon shown when a service has no icon set</p>
|
||||
<IconPicker
|
||||
value={homeData.services_fallback_icon && homeData.services_fallback_icon.trim() ? homeData.services_fallback_icon : 'Award'}
|
||||
onChange={(icon) => setHomeData({ ...homeData, services_fallback_icon: icon })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2371,7 +2434,7 @@ const PageContentDashboard: React.FC = () => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<IconPicker
|
||||
value={experience?.icon || ''}
|
||||
value={experience?.icon && experience.icon.trim() ? experience.icon : 'Sparkles'}
|
||||
onChange={(iconName) => {
|
||||
const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
|
||||
current[index] = { ...current[index], icon: iconName };
|
||||
@@ -2522,7 +2585,7 @@ const PageContentDashboard: React.FC = () => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<IconPicker
|
||||
value={award?.icon || ''}
|
||||
value={award?.icon && award.icon.trim() ? award.icon : 'Award'}
|
||||
onChange={(iconName) => {
|
||||
const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
|
||||
current[index] = { ...current[index], icon: iconName };
|
||||
@@ -3607,6 +3670,112 @@ const PageContentDashboard: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">Icons</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Hero Section Icon</label>
|
||||
<IconPicker
|
||||
value={contactData.contact_icons?.hero && contactData.contact_icons.hero.trim() ? contactData.contact_icons.hero : 'Mail'}
|
||||
onChange={(icon) => setContactData({
|
||||
...contactData,
|
||||
contact_icons: { ...contactData.contact_icons, hero: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Email Contact Icon</label>
|
||||
<IconPicker
|
||||
value={contactData.contact_icons?.email && contactData.contact_icons.email.trim() ? contactData.contact_icons.email : 'Mail'}
|
||||
onChange={(icon) => setContactData({
|
||||
...contactData,
|
||||
contact_icons: { ...contactData.contact_icons, email: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Phone Contact Icon</label>
|
||||
<IconPicker
|
||||
value={contactData.contact_icons?.phone && contactData.contact_icons.phone.trim() ? contactData.contact_icons.phone : 'Phone'}
|
||||
onChange={(icon) => setContactData({
|
||||
...contactData,
|
||||
contact_icons: { ...contactData.contact_icons, phone: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Location Contact Icon</label>
|
||||
<IconPicker
|
||||
value={contactData.contact_icons?.location && contactData.contact_icons.location.trim() ? contactData.contact_icons.location : 'MapPin'}
|
||||
onChange={(icon) => setContactData({
|
||||
...contactData,
|
||||
contact_icons: { ...contactData.contact_icons, location: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Name Field Icon</label>
|
||||
<IconPicker
|
||||
value={contactData.contact_icons?.name_field && contactData.contact_icons.name_field.trim() ? contactData.contact_icons.name_field : 'User'}
|
||||
onChange={(icon) => setContactData({
|
||||
...contactData,
|
||||
contact_icons: { ...contactData.contact_icons, name_field: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Email Field Icon</label>
|
||||
<IconPicker
|
||||
value={contactData.contact_icons?.email_field && contactData.contact_icons.email_field.trim() ? contactData.contact_icons.email_field : 'Mail'}
|
||||
onChange={(icon) => setContactData({
|
||||
...contactData,
|
||||
contact_icons: { ...contactData.contact_icons, email_field: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Phone Field Icon</label>
|
||||
<IconPicker
|
||||
value={contactData.contact_icons?.phone_field && contactData.contact_icons.phone_field.trim() ? contactData.contact_icons.phone_field : 'Phone'}
|
||||
onChange={(icon) => setContactData({
|
||||
...contactData,
|
||||
contact_icons: { ...contactData.contact_icons, phone_field: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Subject Field Icon</label>
|
||||
<IconPicker
|
||||
value={contactData.contact_icons?.subject_field && contactData.contact_icons.subject_field.trim() ? contactData.contact_icons.subject_field : 'MessageSquare'}
|
||||
onChange={(icon) => setContactData({
|
||||
...contactData,
|
||||
contact_icons: { ...contactData.contact_icons, subject_field: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Message Field Icon</label>
|
||||
<IconPicker
|
||||
value={contactData.contact_icons?.message_field && contactData.contact_icons.message_field.trim() ? contactData.contact_icons.message_field : 'MessageSquare'}
|
||||
onChange={(icon) => setContactData({
|
||||
...contactData,
|
||||
contact_icons: { ...contactData.contact_icons, message_field: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Submit Button Icon</label>
|
||||
<IconPicker
|
||||
value={contactData.contact_icons?.submit_button && contactData.contact_icons.submit_button.trim() ? contactData.contact_icons.submit_button : 'Send'}
|
||||
onChange={(icon) => setContactData({
|
||||
...contactData,
|
||||
contact_icons: { ...contactData.contact_icons, submit_button: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
||||
@@ -3727,6 +3896,15 @@ const PageContentDashboard: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hero Icon (when no hero image) */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Hero Icon (shown when no hero image)</label>
|
||||
<IconPicker
|
||||
value={aboutData.about_hero_icon && aboutData.about_hero_icon.trim() ? aboutData.about_hero_icon : 'Hotel'}
|
||||
onChange={(icon) => setAboutData({ ...aboutData, about_hero_icon: icon })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mission */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Mission Statement</label>
|
||||
@@ -3792,7 +3970,7 @@ const PageContentDashboard: React.FC = () => {
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
||||
<IconPicker
|
||||
value={value.icon || 'Heart'}
|
||||
value={value.icon && value.icon.trim() ? value.icon : 'Heart'}
|
||||
onChange={(icon: string) => {
|
||||
setAboutData((prevData) => {
|
||||
const newValues = [...(prevData.values || [])];
|
||||
@@ -3883,7 +4061,7 @@ const PageContentDashboard: React.FC = () => {
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
||||
<IconPicker
|
||||
value={feature.icon || 'Star'}
|
||||
value={feature.icon && feature.icon.trim() ? feature.icon : 'Star'}
|
||||
onChange={(icon: string) => {
|
||||
setAboutData((prevData) => {
|
||||
const newFeatures = [...(prevData.features || [])];
|
||||
@@ -4262,7 +4440,7 @@ const PageContentDashboard: React.FC = () => {
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
||||
<IconPicker
|
||||
value={achievement.icon || 'Award'}
|
||||
value={achievement.icon && achievement.icon.trim() ? achievement.icon : 'Award'}
|
||||
onChange={(icon: string) => {
|
||||
setAboutData((prevData) => {
|
||||
const newAchievements = [...(prevData.achievements || [])];
|
||||
@@ -4378,6 +4556,50 @@ const PageContentDashboard: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Additional Icons */}
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">Additional Icons</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Contact Info - Location Icon</label>
|
||||
<IconPicker
|
||||
value={aboutData.about_contact_icons?.location && aboutData.about_contact_icons.location.trim() ? aboutData.about_contact_icons.location : 'MapPin'}
|
||||
onChange={(icon) => setAboutData({
|
||||
...aboutData,
|
||||
about_contact_icons: { ...aboutData.about_contact_icons, location: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Contact Info - Phone Icon</label>
|
||||
<IconPicker
|
||||
value={aboutData.about_contact_icons?.phone && aboutData.about_contact_icons.phone.trim() ? aboutData.about_contact_icons.phone : 'Phone'}
|
||||
onChange={(icon) => setAboutData({
|
||||
...aboutData,
|
||||
about_contact_icons: { ...aboutData.about_contact_icons, phone: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Contact Info - Email Icon</label>
|
||||
<IconPicker
|
||||
value={aboutData.about_contact_icons?.email && aboutData.about_contact_icons.email.trim() ? aboutData.about_contact_icons.email : 'Mail'}
|
||||
onChange={(icon) => setAboutData({
|
||||
...aboutData,
|
||||
about_contact_icons: { ...aboutData.about_contact_icons, email: icon }
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Learn More Button Icon</label>
|
||||
<IconPicker
|
||||
value={aboutData.about_learn_more_icon && aboutData.about_learn_more_icon.trim() ? aboutData.about_learn_more_icon : 'Hotel'}
|
||||
onChange={(icon) => setAboutData({ ...aboutData, about_learn_more_icon: icon })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
||||
|
||||
@@ -134,6 +134,10 @@ const SettingsPage: React.FC = () => {
|
||||
tax_rate: 0,
|
||||
chat_working_hours_start: 9,
|
||||
chat_working_hours_end: 17,
|
||||
bank_name: '',
|
||||
bank_account_number: '',
|
||||
bank_account_holder: '',
|
||||
bank_code: '',
|
||||
});
|
||||
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||
const [faviconPreview, setFaviconPreview] = useState<string | null>(null);
|
||||
@@ -156,6 +160,7 @@ const SettingsPage: React.FC = () => {
|
||||
theme_primary_light: '#f5d76e',
|
||||
theme_primary_dark: '#c9a227',
|
||||
theme_primary_accent: '#e8c547',
|
||||
theme_layout_mode: 'dark',
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -165,7 +170,6 @@ const SettingsPage: React.FC = () => {
|
||||
const [openPaymentModal, setOpenPaymentModal] = useState<'stripe' | 'paypal' | 'borica' | null>(null);
|
||||
|
||||
const currencyNames: Record<string, string> = {
|
||||
VND: 'Vietnamese Dong',
|
||||
USD: 'US Dollar',
|
||||
EUR: 'Euro',
|
||||
GBP: 'British Pound',
|
||||
@@ -300,6 +304,10 @@ const SettingsPage: React.FC = () => {
|
||||
tax_rate: companyRes.data.tax_rate || 0,
|
||||
chat_working_hours_start: companyRes.data.chat_working_hours_start || 9,
|
||||
chat_working_hours_end: companyRes.data.chat_working_hours_end || 17,
|
||||
bank_name: companyRes.data.bank_name || '',
|
||||
bank_account_number: companyRes.data.bank_account_number || '',
|
||||
bank_account_holder: companyRes.data.bank_account_holder || '',
|
||||
bank_code: companyRes.data.bank_code || '',
|
||||
});
|
||||
|
||||
|
||||
@@ -804,6 +812,7 @@ const SettingsPage: React.FC = () => {
|
||||
theme_primary_light: themeRes.data.theme_primary_light || '#f5d76e',
|
||||
theme_primary_dark: themeRes.data.theme_primary_dark || '#c9a227',
|
||||
theme_primary_accent: themeRes.data.theme_primary_accent || '#e8c547',
|
||||
theme_layout_mode: themeRes.data.theme_layout_mode || 'dark',
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast.error(
|
||||
@@ -2722,6 +2731,82 @@ const SettingsPage: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bank Details Section */}
|
||||
<div className="space-y-4 pt-6 border-t border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="w-5 h-5 text-[var(--luxury-gold)]" />
|
||||
<h3 className="text-lg font-semibold text-gray-900">Bank Transfer Details</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
Configure bank details for bank transfer payments. These will be displayed on payment confirmation pages.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
||||
Bank Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={companyFormData.bank_name || ''}
|
||||
onChange={(e) =>
|
||||
setCompanyFormData({ ...companyFormData, bank_name: e.target.value })
|
||||
}
|
||||
placeholder="e.g., Bank of America, Chase Bank"
|
||||
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
||||
Account Number
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={companyFormData.bank_account_number || ''}
|
||||
onChange={(e) =>
|
||||
setCompanyFormData({ ...companyFormData, bank_account_number: e.target.value })
|
||||
}
|
||||
placeholder="Bank account number"
|
||||
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
||||
Account Holder Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={companyFormData.bank_account_holder || ''}
|
||||
onChange={(e) =>
|
||||
setCompanyFormData({ ...companyFormData, bank_account_holder: e.target.value })
|
||||
}
|
||||
placeholder="Name on the bank account"
|
||||
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
||||
Bank Code (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={companyFormData.bank_code || ''}
|
||||
onChange={(e) =>
|
||||
setCompanyFormData({ ...companyFormData, bank_code: e.target.value })
|
||||
}
|
||||
placeholder="Bank routing/swift code (if needed for QR codes)"
|
||||
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Optional: Bank code used for QR code generation
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
||||
@@ -3124,14 +3209,79 @@ const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layout Mode Section */}
|
||||
<div className="pt-6 border-t border-gray-200">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide mb-3">
|
||||
<Palette className="w-4 h-4 text-gray-600" />
|
||||
Page Layout Theme
|
||||
</label>
|
||||
<p className="text-xs text-gray-600 mb-4">
|
||||
Choose between dark (black) or light (white) background theme for all frontend pages
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setThemeFormData({ ...themeFormData, theme_layout_mode: 'dark' })}
|
||||
className={`p-6 rounded-xl border-2 transition-all duration-200 ${
|
||||
themeFormData.theme_layout_mode === 'dark'
|
||||
? 'border-amber-500 bg-amber-50 shadow-lg'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-16 h-16 rounded-lg bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] border-2 border-gray-300 shadow-inner"></div>
|
||||
<div className="text-center">
|
||||
<p className={`font-semibold text-sm ${themeFormData.theme_layout_mode === 'dark' ? 'text-amber-700' : 'text-gray-700'}`}>
|
||||
Dark Theme
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Black backgrounds</p>
|
||||
</div>
|
||||
{themeFormData.theme_layout_mode === 'dark' && (
|
||||
<div className="w-5 h-5 rounded-full bg-amber-500 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setThemeFormData({ ...themeFormData, theme_layout_mode: 'light' })}
|
||||
className={`p-6 rounded-xl border-2 transition-all duration-200 ${
|
||||
themeFormData.theme_layout_mode === 'light'
|
||||
? 'border-amber-500 bg-amber-50 shadow-lg'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-16 h-16 rounded-lg bg-gradient-to-br from-gray-50 via-white to-gray-50 border-2 border-gray-300 shadow-inner"></div>
|
||||
<div className="text-center">
|
||||
<p className={`font-semibold text-sm ${themeFormData.theme_layout_mode === 'light' ? 'text-amber-700' : 'text-gray-700'}`}>
|
||||
Light Theme
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">White backgrounds</p>
|
||||
</div>
|
||||
{themeFormData.theme_layout_mode === 'light' && (
|
||||
<div className="w-5 h-5 rounded-full bg-amber-500 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-blue-800">
|
||||
<p className="font-semibold mb-1">About Theme Colors</p>
|
||||
<p className="font-semibold mb-1">About Theme Settings</p>
|
||||
<p className="text-xs">
|
||||
Changes to theme colors will be applied immediately across all frontend pages.
|
||||
Use hex color codes (e.g., #d4af37) for best results. The colors will replace the default gold/yellow theme throughout the application.
|
||||
Changes to theme colors and layout mode will be applied immediately across all frontend pages.
|
||||
Use hex color codes (e.g., #d4af37) for best results. The layout mode controls whether pages use dark (black) or light (white) backgrounds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,12 +39,14 @@ import { getBookingStatusConfig } from '../../shared/utils/bookingUtils';
|
||||
import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils';
|
||||
import { BOOKING_STATUS, PAYMENT_METHOD, PAYMENT_STATUS, PAYMENT_TYPE } from '../../shared/constants/bookingConstants';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { useCompanySettings } from '../../shared/contexts/CompanySettingsContext';
|
||||
|
||||
const BookingSuccessPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
const { userInfo } = useAuthStore();
|
||||
const { settings } = useCompanySettings();
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const [booking, setBooking] = useState<Booking | null>(
|
||||
@@ -260,7 +262,10 @@ const BookingSuccessPage: React.FC = () => {
|
||||
const qrCodeUrl = booking
|
||||
? generateQRCode(
|
||||
booking.booking_number,
|
||||
booking.total_price
|
||||
booking.total_price,
|
||||
settings.bank_code || undefined,
|
||||
settings.bank_account_number || undefined,
|
||||
settings.bank_account_holder || undefined
|
||||
)
|
||||
: null;
|
||||
|
||||
@@ -687,15 +692,15 @@ const BookingSuccessPage: React.FC = () => {
|
||||
>
|
||||
<p>
|
||||
<strong>Bank:</strong>
|
||||
Vietcombank (VCB)
|
||||
{settings.bank_name || 'Not configured'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Account Number:</strong>
|
||||
0123456789
|
||||
{settings.bank_account_number || 'Not configured'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Account Holder:</strong>
|
||||
KHACH SAN ABC
|
||||
{settings.bank_account_holder || 'Not configured'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Amount:</strong>{' '}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { validateBookingId } from '../../shared/utils/routeValidation';
|
||||
import { validateAndHandleBookingOwnership } from '../../shared/utils/ownershipValidation';
|
||||
import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils';
|
||||
import { PAYMENT_METHOD, PAYMENT_STATUS } from '../../shared/constants/bookingConstants';
|
||||
import { useCompanySettings } from '../../shared/contexts/CompanySettingsContext';
|
||||
|
||||
const PaymentConfirmationPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -37,6 +38,7 @@ const PaymentConfirmationPage: React.FC = () => {
|
||||
const { isAuthenticated, userInfo } = useAuthStore();
|
||||
const { openModal } = useAuthModal();
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
const { settings } = useCompanySettings();
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const [booking, setBooking] = useState<Booking | null>(
|
||||
@@ -256,7 +258,10 @@ const PaymentConfirmationPage: React.FC = () => {
|
||||
|
||||
const qrCodeUrl = generateQRCode(
|
||||
booking.booking_number,
|
||||
booking.total_price
|
||||
booking.total_price,
|
||||
settings.bank_code || undefined,
|
||||
settings.bank_account_number || undefined,
|
||||
settings.bank_account_holder || undefined
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -371,15 +376,15 @@ const PaymentConfirmationPage: React.FC = () => {
|
||||
>
|
||||
<p>
|
||||
<strong>Bank:</strong>
|
||||
Vietcombank (VCB)
|
||||
{settings.bank_name || 'Not configured'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Account Number:</strong>
|
||||
0123456789
|
||||
{settings.bank_account_number || 'Not configured'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Account Holder:</strong>
|
||||
KHACH SAN ABC
|
||||
{settings.bank_account_holder || 'Not configured'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Amount:</strong>{' '}
|
||||
@@ -408,12 +413,18 @@ const PaymentConfirmationPage: React.FC = () => {
|
||||
>
|
||||
Scan QR code to transfer
|
||||
</p>
|
||||
<img
|
||||
src={qrCodeUrl}
|
||||
alt="QR Code"
|
||||
className="w-48 h-48 border-2
|
||||
border-gray-200 rounded-lg"
|
||||
/>
|
||||
{qrCodeUrl ? (
|
||||
<img
|
||||
src={qrCodeUrl}
|
||||
alt="QR Code"
|
||||
className="w-48 h-48 border-2
|
||||
border-gray-200 rounded-lg"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 text-center">
|
||||
Bank details not configured. Please contact support.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,8 +17,8 @@ const PaymentResultPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
|
||||
|
||||
const supportEmail = settings.company_email || 'support@hotel.com';
|
||||
const supportPhone = settings.company_phone || '1900 xxxx';
|
||||
const supportEmail = settings.company_email || null;
|
||||
const supportPhone = settings.company_phone || null;
|
||||
|
||||
const status = searchParams.get('status');
|
||||
const bookingId = searchParams.get('bookingId');
|
||||
|
||||
@@ -21,8 +21,14 @@ import LuxuryBookingModal from '../../features/bookings/components/LuxuryBooking
|
||||
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
|
||||
import { toast } from 'react-toastify';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { useTheme } from '../../shared/contexts/ThemeContext';
|
||||
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../shared/utils/themeUtils';
|
||||
|
||||
const RoomDetailPage: React.FC = () => {
|
||||
const { theme } = useTheme();
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
const { room_number } = useParams<{ room_number: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
@@ -172,7 +178,7 @@ const RoomDetailPage: React.FC = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -184,9 +190,9 @@ const RoomDetailPage: React.FC = () => {
|
||||
>
|
||||
<div className="w-full px-2 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12">
|
||||
<div className="animate-pulse space-y-6">
|
||||
<div className="h-[600px] bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20" />
|
||||
<div className="h-12 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg w-1/3 border border-[var(--luxury-gold)]/10" />
|
||||
<div className="h-6 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg w-2/3 border border-[var(--luxury-gold)]/10" />
|
||||
<div className={`h-[600px] ${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20`} />
|
||||
<div className={`h-12 ${cardClasses} rounded-lg w-1/3 border border-[var(--luxury-gold)]/10`} />
|
||||
<div className={`h-6 ${cardClasses} rounded-lg w-2/3 border border-[var(--luxury-gold)]/10`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,7 +202,7 @@ const RoomDetailPage: React.FC = () => {
|
||||
if (error || !room) {
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -236,7 +242,7 @@ const RoomDetailPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
@@ -310,7 +316,7 @@ const RoomDetailPage: React.FC = () => {
|
||||
: 'Maintenance'}
|
||||
</div>
|
||||
{room.status === 'occupied' && bookedUntilDate && (
|
||||
<p className="text-[9px] sm:text-[10px] text-gray-300 font-light leading-tight">
|
||||
<p className={`text-[9px] sm:text-[10px] ${textClasses.secondary} font-light leading-tight`}>
|
||||
Booked until {bookedUntilDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||||
</p>
|
||||
)}
|
||||
@@ -322,10 +328,12 @@ const RoomDetailPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="text-xl sm:text-2xl lg:text-3xl font-serif font-semibold
|
||||
text-white mb-2 tracking-tight leading-tight
|
||||
bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white
|
||||
bg-clip-text text-transparent"
|
||||
<h1 className={`text-xl sm:text-2xl lg:text-3xl font-serif font-semibold
|
||||
${textClasses.primary} mb-2 tracking-tight leading-tight
|
||||
${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'}
|
||||
bg-clip-text text-transparent`}
|
||||
>
|
||||
{roomType?.name}
|
||||
</h1>
|
||||
@@ -334,62 +342,62 @@ const RoomDetailPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 sm:gap-3 mb-3">
|
||||
<div className="flex items-center gap-2
|
||||
p-2 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
||||
<div className={`flex items-center gap-2
|
||||
p-2 ${cardClasses}
|
||||
rounded-lg border border-[var(--luxury-gold)]/20
|
||||
hover:border-[var(--luxury-gold)]/40 transition-all duration-300"
|
||||
hover:border-[var(--luxury-gold)]/40 transition-all duration-300`}
|
||||
>
|
||||
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||
border border-[var(--luxury-gold)]/30">
|
||||
<MapPin className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide mb-0.5">
|
||||
<p className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide mb-0.5`}>
|
||||
Location
|
||||
</p>
|
||||
<p className="text-xs sm:text-sm text-white font-light tracking-wide">
|
||||
<p className={`text-xs sm:text-sm ${textClasses.primary} font-light tracking-wide`}>
|
||||
Room {room.room_number} - Floor {room.floor}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2
|
||||
p-2 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
||||
rounded-lg border border-[var(--luxury-gold)]/20"
|
||||
<div className={`flex items-center gap-2
|
||||
p-2 ${cardClasses}
|
||||
rounded-lg border border-[var(--luxury-gold)]/20`}
|
||||
>
|
||||
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||
border border-[var(--luxury-gold)]/30">
|
||||
<Users className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide mb-0.5">
|
||||
<p className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide mb-0.5`}>
|
||||
Capacity
|
||||
</p>
|
||||
<p className="text-xs sm:text-sm text-white font-light tracking-wide">
|
||||
<p className={`text-xs sm:text-sm ${textClasses.primary} font-light tracking-wide`}>
|
||||
{room?.capacity || roomType?.capacity || 0} guests
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{room.average_rating != null && (
|
||||
<div className="flex items-center gap-2
|
||||
p-2 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
||||
<div className={`flex items-center gap-2
|
||||
p-2 ${cardClasses}
|
||||
rounded-lg border border-[var(--luxury-gold)]/20
|
||||
hover:border-[var(--luxury-gold)]/40 transition-all duration-300"
|
||||
hover:border-[var(--luxury-gold)]/40 transition-all duration-300`}
|
||||
>
|
||||
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||
border border-[var(--luxury-gold)]/30">
|
||||
<Star className="w-3.5 h-3.5 text-[var(--luxury-gold)] fill-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide mb-0.5">
|
||||
<p className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide mb-0.5`}>
|
||||
Rating
|
||||
</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xs sm:text-sm text-white font-semibold">
|
||||
<p className={`text-xs sm:text-sm ${textClasses.primary} font-semibold`}>
|
||||
{Number(room.average_rating).toFixed(1)}
|
||||
</p>
|
||||
<span className="text-[10px] sm:text-xs text-gray-500 font-light">
|
||||
<span className={`text-[10px] sm:text-xs ${textClasses.muted} font-light`}>
|
||||
({room.total_reviews || 0})
|
||||
</span>
|
||||
</div>
|
||||
@@ -401,23 +409,23 @@ const RoomDetailPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
{(room?.description || roomType?.description) && (
|
||||
<div className="p-3 sm:p-4 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
||||
<div className={`p-3 sm:p-4 ${cardClasses}
|
||||
rounded-lg border border-[var(--luxury-gold)]/20
|
||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5"
|
||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||
border border-[var(--luxury-gold)]/30">
|
||||
<Award className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<h2 className="text-sm sm:text-base font-serif font-semibold
|
||||
text-white tracking-wide"
|
||||
<h2 className={`text-sm sm:text-base font-serif font-semibold
|
||||
${textClasses.primary} tracking-wide`}
|
||||
>
|
||||
{room?.description ? 'Room Description' : 'Room Type Description'}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-gray-300 leading-relaxed
|
||||
font-light tracking-wide text-xs sm:text-sm"
|
||||
<p className={`${textClasses.secondary} leading-relaxed
|
||||
font-light tracking-wide text-xs sm:text-sm`}
|
||||
>
|
||||
{room?.description || roomType?.description}
|
||||
</p>
|
||||
@@ -425,17 +433,17 @@ const RoomDetailPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{}
|
||||
<div className="p-3 sm:p-4 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
||||
<div className={`p-3 sm:p-4 ${cardClasses}
|
||||
rounded-lg border border-[var(--luxury-gold)]/20
|
||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5"
|
||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||
border border-[var(--luxury-gold)]/30">
|
||||
<Sparkles className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<h2 className="text-sm sm:text-base font-serif font-semibold
|
||||
text-white tracking-wide"
|
||||
<h2 className={`text-sm sm:text-base font-serif font-semibold
|
||||
${textClasses.primary} tracking-wide`}
|
||||
>
|
||||
Amenities & Features
|
||||
</h2>
|
||||
@@ -461,14 +469,14 @@ const RoomDetailPage: React.FC = () => {
|
||||
|
||||
{}
|
||||
<aside className="lg:col-span-4">
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a]
|
||||
<div className={`${cardClasses}
|
||||
rounded-lg border border-[var(--luxury-gold)]/30
|
||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/20
|
||||
p-3 sm:p-4 sticky top-4"
|
||||
p-3 sm:p-4 sticky top-4`}
|
||||
>
|
||||
{}
|
||||
<div className="mb-4 pb-4 border-b border-[var(--luxury-gold)]/20">
|
||||
<p className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide mb-1">
|
||||
<p className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide mb-1`}>
|
||||
Starting from
|
||||
</p>
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
@@ -480,7 +488,7 @@ const RoomDetailPage: React.FC = () => {
|
||||
>
|
||||
{formattedPrice}
|
||||
</div>
|
||||
<div className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide mt-0.5">
|
||||
<div className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide mt-0.5`}>
|
||||
/ night
|
||||
</div>
|
||||
</div>
|
||||
@@ -529,7 +537,7 @@ const RoomDetailPage: React.FC = () => {
|
||||
rounded-lg border border-[var(--luxury-gold)]/20 mb-3"
|
||||
>
|
||||
<Shield className="w-3.5 h-3.5 text-[var(--luxury-gold)] 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 ${textClasses.secondary} font-light tracking-wide leading-relaxed`}>
|
||||
<strong className="text-[var(--luxury-gold)]">20% deposit required</strong> to secure your booking. Pay the remaining balance on arrival at the hotel.
|
||||
</p>
|
||||
</div>
|
||||
@@ -542,27 +550,27 @@ const RoomDetailPage: React.FC = () => {
|
||||
<div className="flex items-center justify-between
|
||||
py-1.5 border-b border-[var(--luxury-gold)]/10"
|
||||
>
|
||||
<span className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide">Room Type</span>
|
||||
<strong className="text-xs sm:text-sm text-white font-light">{roomType?.name}</strong>
|
||||
<span className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide`}>Room Type</span>
|
||||
<strong className={`text-xs sm:text-sm ${textClasses.primary} font-light`}>{roomType?.name}</strong>
|
||||
</div>
|
||||
<div className="flex items-center justify-between
|
||||
py-1.5 border-b border-[var(--luxury-gold)]/10"
|
||||
>
|
||||
<span className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide">Max Guests</span>
|
||||
<span className="text-xs sm:text-sm text-white font-light">{(room?.capacity || roomType?.capacity || 0)} guests</span>
|
||||
<span className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide`}>Max Guests</span>
|
||||
<span className={`text-xs sm:text-sm ${textClasses.primary} font-light`}>{(room?.capacity || roomType?.capacity || 0)} guests</span>
|
||||
</div>
|
||||
{room?.room_size && (
|
||||
<div className="flex items-center justify-between
|
||||
py-1.5 border-b border-[var(--luxury-gold)]/10"
|
||||
>
|
||||
<span className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide">Room Size</span>
|
||||
<span className="text-xs sm:text-sm text-white font-light">{room.room_size}</span>
|
||||
<span className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide`}>Room Size</span>
|
||||
<span className={`text-xs sm:text-sm ${textClasses.primary} font-light`}>{room.room_size}</span>
|
||||
</div>
|
||||
)}
|
||||
{room?.view && (
|
||||
<div className={`flex items-center justify-between ${room?.room_size ? 'py-1.5 border-b border-[var(--luxury-gold)]/10' : 'py-1.5'}`}>
|
||||
<span className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide">View</span>
|
||||
<span className="text-xs sm:text-sm text-white font-light">{room.view}</span>
|
||||
<span className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide`}>View</span>
|
||||
<span className={`text-xs sm:text-sm ${textClasses.primary} font-light`}>{room.view}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -571,9 +579,9 @@ const RoomDetailPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="mb-4 p-3 sm:p-4 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
||||
<div className={`mb-4 p-3 sm:p-4 ${cardClasses}
|
||||
rounded-lg border border-[var(--luxury-gold)]/20
|
||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5"
|
||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5`}
|
||||
>
|
||||
<ReviewSection roomId={room.id} />
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,15 @@ import Pagination from '../../shared/components/Pagination';
|
||||
import { ArrowLeft, Hotel, Filter, ChevronDown, ChevronUp, Tag, X, CheckCircle } from 'lucide-react';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useTheme } from '../../shared/contexts/ThemeContext';
|
||||
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../shared/utils/themeUtils';
|
||||
|
||||
const RoomListPage: React.FC = () => {
|
||||
const { theme } = useTheme();
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -146,13 +153,13 @@ const RoomListPage: React.FC = () => {
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||
{}
|
||||
{/* Promotion Banner */}
|
||||
{showPromotionBanner && activePromotion && (
|
||||
<div className="w-full bg-gradient-to-r from-[var(--luxury-gold)]/20 via-[var(--luxury-gold-light)]/15 to-[var(--luxury-gold)]/20 border-b border-[var(--luxury-gold)]/30">
|
||||
<div className="w-full max-w-[1920px] mx-auto px-3 sm:px-4 md:px-6 lg:px-8 xl:px-12 2xl:px-16 3xl:px-20 py-4">
|
||||
<div className="flex items-center justify-between gap-4 bg-gradient-to-r from-[#1a1a1a] to-[#0f0f0f] border border-[var(--luxury-gold)]/40 rounded-lg p-4 backdrop-blur-xl shadow-lg">
|
||||
<div className={`flex items-center justify-between gap-4 ${cardClasses} border border-[var(--luxury-gold)]/40 rounded-lg p-4 backdrop-blur-xl shadow-lg`}>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<div className="p-2 bg-[var(--luxury-gold)]/20 rounded-lg border border-[var(--luxury-gold)]/40">
|
||||
<Tag className="w-5 h-5 text-[var(--luxury-gold)]" />
|
||||
@@ -165,11 +172,11 @@ const RoomListPage: React.FC = () => {
|
||||
</span>
|
||||
</div>
|
||||
{activePromotion.discount && (
|
||||
<p className="text-xs text-gray-300">
|
||||
<p className={`text-xs ${textClasses.secondary}`}>
|
||||
{activePromotion.discount} - {activePromotion.description || 'Valid on bookings'}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
<p className={`text-xs ${textClasses.muted} mt-1`}>
|
||||
The promotion code will be automatically applied when you book a room
|
||||
</p>
|
||||
</div>
|
||||
@@ -179,14 +186,14 @@ const RoomListPage: React.FC = () => {
|
||||
className="p-2 hover:bg-[var(--luxury-gold)]/10 rounded-lg transition-colors"
|
||||
aria-label="Dismiss promotion"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-400 hover:text-white" />
|
||||
<X className={`w-5 h-5 ${textClasses.muted} ${theme.theme_layout_mode === 'light' ? 'hover:text-gray-900' : 'hover:text-white'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8">
|
||||
<div className={`w-full ${heroBgClasses} border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8`}>
|
||||
<div className="w-full max-w-[1920px] mx-auto px-3 sm:px-4 md:px-6 lg:px-8 xl:px-12 2xl:px-16 3xl:px-20 py-6 sm:py-7 md:py-8 lg:py-10">
|
||||
{}
|
||||
<Link
|
||||
@@ -212,14 +219,16 @@ const RoomListPage: React.FC = () => {
|
||||
<Hotel className="w-5 h-5 sm:w-5 sm:h-5 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold
|
||||
text-white mb-2 sm:mb-3 tracking-tight leading-tight px-2
|
||||
bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white
|
||||
bg-clip-text text-transparent"
|
||||
<h1 className={`text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold
|
||||
${textClasses.primary} mb-2 sm:mb-3 tracking-tight leading-tight px-2
|
||||
${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'}
|
||||
bg-clip-text text-transparent`}
|
||||
>
|
||||
Our Rooms & Suites
|
||||
</h1>
|
||||
<p className="text-gray-400 font-light tracking-wide text-xs sm:text-sm md:text-base max-w-xl mx-auto px-2 sm:px-4 leading-relaxed">
|
||||
<p className={`${textClasses.muted} font-light tracking-wide text-xs sm:text-sm md:text-base max-w-xl mx-auto px-2 sm:px-4 leading-relaxed`}>
|
||||
Discover our collection of luxurious accommodations,
|
||||
each designed to provide an exceptional stay
|
||||
</p>
|
||||
@@ -235,18 +244,18 @@ const RoomListPage: React.FC = () => {
|
||||
<div className="xl:hidden order-1 mb-4">
|
||||
<button
|
||||
onClick={() => setIsFilterOpen(!isFilterOpen)}
|
||||
className="w-full bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
||||
className={`w-full ${cardClasses}
|
||||
border border-[var(--luxury-gold)]/30 rounded-xl p-4
|
||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/10
|
||||
flex items-center justify-between gap-3
|
||||
hover:border-[var(--luxury-gold)]/50 hover:shadow-xl hover:shadow-[var(--luxury-gold)]/20
|
||||
transition-all duration-300 touch-manipulation"
|
||||
transition-all duration-300 touch-manipulation`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-[var(--luxury-gold)]/10 rounded-lg border border-[var(--luxury-gold)]/30">
|
||||
<Filter className="w-5 h-5 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<span className="text-white font-medium tracking-wide text-base">
|
||||
<span className={`${textClasses.primary} font-medium tracking-wide text-base`}>
|
||||
Filters
|
||||
</span>
|
||||
</div>
|
||||
@@ -320,21 +329,21 @@ const RoomListPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{!loading && !error && rooms.length === 0 && (
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
||||
<div className={`${cardClasses}
|
||||
border border-[var(--luxury-gold)]/20 rounded-xl p-8 sm:p-10 md:p-12 lg:p-16 text-center
|
||||
backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5"
|
||||
backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5`}
|
||||
>
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24
|
||||
bg-[var(--luxury-gold)]/10 rounded-2xl mb-4 sm:mb-5 md:mb-6 lg:mb-8 border border-[var(--luxury-gold)]/30"
|
||||
>
|
||||
<Hotel className="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 text-[var(--luxury-gold)]" />
|
||||
</div>
|
||||
<h3 className="text-lg sm:text-xl md:text-2xl font-serif font-semibold
|
||||
text-white mb-3 sm:mb-4 tracking-wide px-2"
|
||||
<h3 className={`text-lg sm:text-xl md:text-2xl font-serif font-semibold
|
||||
${textClasses.primary} mb-3 sm:mb-4 tracking-wide px-2`}
|
||||
>
|
||||
No matching rooms found
|
||||
</h3>
|
||||
<p className="text-gray-400 font-light tracking-wide mb-5 sm:mb-6 md:mb-8 text-sm sm:text-base md:text-lg px-2">
|
||||
<p className={`${textClasses.muted} font-light tracking-wide mb-5 sm:mb-6 md:mb-8 text-sm sm:text-base md:text-lg px-2`}>
|
||||
Please try adjusting the filters or search differently
|
||||
</p>
|
||||
<button
|
||||
@@ -354,7 +363,7 @@ const RoomListPage: React.FC = () => {
|
||||
<>
|
||||
{}
|
||||
<div className="mb-3 sm:mb-4 md:mb-5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-3">
|
||||
<p className="text-gray-400 font-light tracking-wide text-xs sm:text-sm md:text-base">
|
||||
<p className={`${textClasses.muted} font-light tracking-wide text-xs sm:text-sm md:text-base`}>
|
||||
Showing <span className="text-[var(--luxury-gold)] font-medium">{rooms.length}</span> of{' '}
|
||||
<span className="text-[var(--luxury-gold)] font-medium">{pagination.total}</span> rooms
|
||||
</p>
|
||||
|
||||
@@ -14,7 +14,7 @@ const CurrencyIcon: React.FC<CurrencyIconProps> = ({
|
||||
currency
|
||||
}) => {
|
||||
const { currency: contextCurrency } = useCurrency();
|
||||
const currencyToUse = currency || contextCurrency || 'VND';
|
||||
const currencyToUse = currency || contextCurrency || 'USD';
|
||||
const symbol = getCurrencySymbol(currencyToUse);
|
||||
|
||||
return (
|
||||
|
||||
@@ -25,7 +25,6 @@ const CurrencySelector: React.FC<CurrencySelectorProps> = ({
|
||||
const isAdmin = userInfo?.role === 'admin';
|
||||
|
||||
const currencyNames: Record<string, string> = {
|
||||
VND: 'Vietnamese Dong',
|
||||
USD: 'US Dollar',
|
||||
EUR: 'Euro',
|
||||
GBP: 'British Pound',
|
||||
|
||||
@@ -29,11 +29,18 @@ import CookiePreferencesLink from './CookiePreferencesLink';
|
||||
import ChatWidget from '../../features/notifications/components/ChatWidget';
|
||||
import pageContentService, { type PageContent } from '../../features/content/services/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import apiClient from '../services/apiClient';
|
||||
import { formatWorkingHours } from '../utils/format';
|
||||
import { Clock } from 'lucide-react';
|
||||
import { getThemeTextClasses, getThemeCardClasses } from '../utils/themeUtils';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const { theme } = useTheme();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||
const [homePageContent, setHomePageContent] = useState<PageContent | null>(null);
|
||||
const [enabledPages, setEnabledPages] = useState<Set<string>>(new Set());
|
||||
const [apiError, setApiError] = useState(false);
|
||||
@@ -192,7 +199,9 @@ const Footer: React.FC = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<footer className="relative bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black text-gray-300 overflow-hidden">
|
||||
<footer className={`relative ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-b from-gray-50 via-white to-gray-100'
|
||||
: 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black'} ${textClasses.secondary} overflow-hidden`}>
|
||||
{/* Top Gold Accent Line */}
|
||||
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent shadow-lg shadow-[var(--luxury-gold)]/50"></div>
|
||||
|
||||
@@ -228,7 +237,7 @@ const Footer: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h2 className="text-2xl sm:text-3xl font-display font-semibold text-white tracking-tight mb-1">
|
||||
<h2 className={`text-2xl sm:text-3xl font-display font-semibold ${textClasses.primary} tracking-tight mb-1`}>
|
||||
{settings.company_name || pageContent?.title || 'Luxury Hotel'}
|
||||
</h2>
|
||||
<p className="text-xs sm:text-sm text-[var(--luxury-gold)] font-light tracking-[3px] sm:tracking-[4px] uppercase">
|
||||
@@ -236,7 +245,7 @@ const Footer: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm sm:text-base text-gray-400 mb-8 leading-relaxed max-w-md font-light">
|
||||
<p className={`text-sm sm:text-base ${textClasses.muted} 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.'}
|
||||
</p>
|
||||
|
||||
@@ -252,7 +261,7 @@ const Footer: React.FC = () => {
|
||||
className="group flex items-center space-x-2 px-3 py-2 bg-gradient-to-r from-[var(--luxury-gold)]/5 to-transparent border border-[var(--luxury-gold)]/10 rounded-lg hover:border-[var(--luxury-gold)]/30 hover:from-[var(--luxury-gold)]/10 transition-all duration-300"
|
||||
>
|
||||
<BadgeIcon className="w-4 h-4 sm:w-5 sm:h-5 text-[var(--luxury-gold)] 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-[var(--luxury-gold)] transition-colors">{badge.text}</span>
|
||||
<span className={`text-xs sm:text-sm font-medium tracking-wide ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-colors`}>{badge.text}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -266,10 +275,12 @@ const Footer: React.FC = () => {
|
||||
href={pageContent.social_links.facebook}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
|
||||
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-white border border-gray-300 shadow-sm'
|
||||
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
|
||||
aria-label="Facebook"
|
||||
>
|
||||
<Facebook className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
||||
<Facebook className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
|
||||
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
||||
</a>
|
||||
)}
|
||||
@@ -278,10 +289,12 @@ const Footer: React.FC = () => {
|
||||
href={pageContent.social_links.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
|
||||
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-white border border-gray-300 shadow-sm'
|
||||
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
|
||||
aria-label="Twitter"
|
||||
>
|
||||
<Twitter className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
||||
<Twitter className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
|
||||
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
||||
</a>
|
||||
)}
|
||||
@@ -290,10 +303,12 @@ const Footer: React.FC = () => {
|
||||
href={pageContent.social_links.instagram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
|
||||
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-white border border-gray-300 shadow-sm'
|
||||
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
|
||||
aria-label="Instagram"
|
||||
>
|
||||
<Instagram className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
||||
<Instagram className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
|
||||
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
||||
</a>
|
||||
)}
|
||||
@@ -302,10 +317,12 @@ const Footer: React.FC = () => {
|
||||
href={pageContent.social_links.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
|
||||
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-white border border-gray-300 shadow-sm'
|
||||
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
|
||||
aria-label="LinkedIn"
|
||||
>
|
||||
<Linkedin className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
||||
<Linkedin className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
|
||||
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
||||
</a>
|
||||
)}
|
||||
@@ -314,10 +331,12 @@ const Footer: React.FC = () => {
|
||||
href={pageContent.social_links.youtube}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
|
||||
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-white border border-gray-300 shadow-sm'
|
||||
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
|
||||
aria-label="YouTube"
|
||||
>
|
||||
<Youtube className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
||||
<Youtube className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
|
||||
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
||||
</a>
|
||||
)}
|
||||
@@ -327,7 +346,7 @@ const Footer: React.FC = () => {
|
||||
{/* Quick Links */}
|
||||
{quickLinks.length > 0 && (
|
||||
<div className="lg:col-span-2">
|
||||
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide">
|
||||
<h3 className={`${textClasses.primary} font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide`}>
|
||||
<span className="relative z-10">Quick Links</span>
|
||||
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
||||
</h3>
|
||||
@@ -336,7 +355,7 @@ const Footer: React.FC = () => {
|
||||
<li key={link.url}>
|
||||
<Link
|
||||
to={link.url}
|
||||
className="group flex items-center text-sm sm:text-base text-gray-400 hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide"
|
||||
className={`group flex items-center text-sm sm:text-base ${textClasses.muted} hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide`}
|
||||
>
|
||||
<span className="absolute left-0 w-0 h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] group-hover:w-8 transition-all duration-300 rounded-full"></span>
|
||||
<span className="ml-10 group-hover:translate-x-2 transition-transform duration-300 group-hover:font-medium">{link.label}</span>
|
||||
@@ -350,7 +369,7 @@ const Footer: React.FC = () => {
|
||||
{/* Guest Services */}
|
||||
{supportLinks.length > 0 && (
|
||||
<div className="lg:col-span-2">
|
||||
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide">
|
||||
<h3 className={`${textClasses.primary} font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide`}>
|
||||
<span className="relative z-10">Guest Services</span>
|
||||
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
||||
</h3>
|
||||
@@ -359,7 +378,7 @@ const Footer: React.FC = () => {
|
||||
<li key={link.url}>
|
||||
<Link
|
||||
to={link.url}
|
||||
className="group flex items-center text-sm sm:text-base text-gray-400 hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide"
|
||||
className={`group flex items-center text-sm sm:text-base ${textClasses.muted} hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide`}
|
||||
>
|
||||
<span className="absolute left-0 w-0 h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] group-hover:w-8 transition-all duration-300 rounded-full"></span>
|
||||
<span className="ml-10 group-hover:translate-x-2 transition-transform duration-300 group-hover:font-medium">{link.label}</span>
|
||||
@@ -377,7 +396,7 @@ const Footer: React.FC = () => {
|
||||
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
||||
</h3>
|
||||
{homePageContent?.newsletter_section_subtitle && (
|
||||
<p className="text-sm text-gray-400 mb-4 font-light leading-relaxed">
|
||||
<p className={`text-sm ${textClasses.muted} mb-4 font-light leading-relaxed`}>
|
||||
{homePageContent.newsletter_section_subtitle}
|
||||
</p>
|
||||
)}
|
||||
@@ -416,7 +435,9 @@ const Footer: React.FC = () => {
|
||||
value={newsletterEmail}
|
||||
onChange={(e) => setNewsletterEmail(e.target.value)}
|
||||
placeholder={homePageContent?.newsletter_placeholder || 'Enter your email'}
|
||||
className="w-full px-4 py-2.5 rounded-lg border border-gray-700 bg-gray-800/50 text-white placeholder-gray-400 focus:border-[var(--luxury-gold)] focus:ring-2 focus:ring-[var(--luxury-gold)]/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
||||
className={`w-full px-4 py-2.5 rounded-lg border ${theme.theme_layout_mode === 'light'
|
||||
? 'border-gray-300 bg-white text-gray-900 placeholder-gray-500'
|
||||
: 'border-gray-700 bg-gray-800/50 text-white placeholder-gray-400'} focus:border-[var(--luxury-gold)] focus:ring-2 focus:ring-[var(--luxury-gold)]/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm`}
|
||||
required
|
||||
disabled={newsletterSubmitting}
|
||||
/>
|
||||
@@ -441,7 +462,7 @@ const Footer: React.FC = () => {
|
||||
|
||||
{/* Contact Information */}
|
||||
<div className="lg:col-span-2">
|
||||
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide">
|
||||
<h3 className={`${textClasses.primary} font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide`}>
|
||||
<span className="relative z-10">Contact</span>
|
||||
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
||||
</h3>
|
||||
@@ -455,7 +476,7 @@ const Footer: React.FC = () => {
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
</div>
|
||||
<span className="text-sm sm:text-base text-gray-400 group-hover:text-gray-300 transition-colors leading-relaxed font-light pt-1">
|
||||
<span className={`text-sm sm:text-base ${textClasses.muted} group-hover:${textClasses.secondary} transition-colors leading-relaxed font-light pt-1`}>
|
||||
{displayAddress
|
||||
.split('\n').map((line, i) => (
|
||||
<React.Fragment key={i}>
|
||||
@@ -474,7 +495,7 @@ const Footer: React.FC = () => {
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
</div>
|
||||
<a href={phoneHref} className="text-sm sm:text-base text-gray-400 group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide">
|
||||
<a href={phoneHref} className={`text-sm sm:text-base ${textClasses.muted} group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide`}>
|
||||
{displayPhone}
|
||||
</a>
|
||||
</li>
|
||||
@@ -487,11 +508,29 @@ const Footer: React.FC = () => {
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
</div>
|
||||
<a href={`mailto:${displayEmail}`} className="text-sm sm:text-base text-gray-400 group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide break-all">
|
||||
<a href={`mailto:${displayEmail}`} className={`text-sm sm:text-base ${textClasses.muted} group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide break-all`}>
|
||||
{displayEmail}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
|
||||
<li className="flex items-start space-x-4 group">
|
||||
<div className="relative mt-1 flex-shrink-0">
|
||||
<div className="p-2 bg-gradient-to-br from-[var(--luxury-gold)]/10 to-[var(--luxury-gold-dark)]/5 rounded-lg border border-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/40 transition-all duration-300">
|
||||
<Clock className="w-4 h-4 sm:w-5 sm:h-5 text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<span className={`text-sm sm:text-base ${textClasses.muted} group-hover:${textClasses.secondary} transition-colors font-light tracking-wide block`}>
|
||||
Chat Support Hours
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm text-gray-500 font-light">
|
||||
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
|
||||
@@ -18,10 +18,12 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useClickOutside } from '../hooks/useClickOutside';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
|
||||
import { normalizeImageUrl } from '../utils/imageUtils';
|
||||
import InAppNotificationBell from '../../features/notifications/components/InAppNotificationBell';
|
||||
import Navbar from './Navbar';
|
||||
import { getThemeTextClasses } from '../utils/themeUtils';
|
||||
|
||||
interface HeaderProps {
|
||||
isAuthenticated?: boolean;
|
||||
@@ -40,12 +42,14 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onLogout
|
||||
}) => {
|
||||
const { settings } = useCompanySettings();
|
||||
const { theme } = useTheme();
|
||||
const { openModal } = useAuthModal();
|
||||
const location = useLocation();
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
|
||||
|
||||
const displayPhone = settings.company_phone || '+1 (234) 567-890';
|
||||
const displayEmail = settings.company_email || 'info@luxuryhotel.com';
|
||||
const displayPhone = settings.company_phone || null;
|
||||
const displayEmail = settings.company_email || null;
|
||||
const logoUrl = settings.company_logo_url
|
||||
? (settings.company_logo_url.startsWith('http')
|
||||
? settings.company_logo_url
|
||||
@@ -110,12 +114,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3.5 text-white/95
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3.5 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
|
||||
rounded-md transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wider text-sm w-full text-left group relative mx-2 cursor-pointer"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wider text-sm w-full text-left group relative mx-2 cursor-pointer`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<LogIn className="w-4 h-4" />
|
||||
@@ -160,7 +164,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3.5 text-white/95
|
||||
space-x-2 px-4 py-3.5 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
|
||||
rounded-md transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
@@ -181,12 +185,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<Calendar className="w-4 h-4" />
|
||||
@@ -201,12 +205,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<Heart className="w-4 h-4" />
|
||||
@@ -221,12 +225,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<Calendar className="w-4 h-4" />
|
||||
@@ -241,12 +245,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<Star className="w-4 h-4" />
|
||||
@@ -261,12 +265,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
@@ -281,12 +285,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
@@ -301,12 +305,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
@@ -321,12 +325,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
@@ -344,12 +348,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
@@ -366,12 +370,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
@@ -388,12 +392,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
@@ -410,12 +414,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center
|
||||
space-x-2 px-4 py-3 text-white/90
|
||||
className={`flex items-center
|
||||
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
@@ -446,9 +450,15 @@ const Header: React.FC<HeaderProps> = ({
|
||||
|
||||
return (
|
||||
<header
|
||||
className="bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] sticky top-0 z-50 border-b border-[rgba(var(--luxury-gold-rgb),0.15)] shadow-[0_8px_32px_rgba(0,0,0,0.4)] backdrop-blur-sm"
|
||||
className={`sticky top-0 z-50 border-b backdrop-blur-sm ${
|
||||
theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-b from-white via-gray-50 to-white border-gray-200 shadow-[0_8px_32px_rgba(0,0,0,0.1)]'
|
||||
: 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] border-[rgba(var(--luxury-gold-rgb),0.15)] shadow-[0_8px_32px_rgba(0,0,0,0.4)]'
|
||||
}`}
|
||||
>
|
||||
<div className="hidden lg:block bg-[#0a0a0a]/50 border-b border-[rgba(var(--luxury-gold-rgb),0.1)]">
|
||||
<div className={`hidden lg:block ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gray-50/80 border-gray-200'
|
||||
: 'bg-[#0a0a0a]/50 border-[rgba(var(--luxury-gold-rgb),0.1)]'} border-b`}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
|
||||
<div className="flex items-center justify-end space-x-6 text-sm">
|
||||
{displayPhone && (
|
||||
@@ -492,7 +502,9 @@ const Header: React.FC<HeaderProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-sm sm:text-base md:text-lg lg:text-xl xl:text-2xl font-display font-semibold text-white tracking-tight leading-tight bg-gradient-to-r from-white to-white/90 bg-clip-text truncate">
|
||||
<span className={`text-sm sm:text-base md:text-lg lg:text-xl xl:text-2xl font-display font-semibold ${textClasses.primary} tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-r from-gray-900 to-gray-700 bg-clip-text'
|
||||
: 'bg-gradient-to-r from-white to-white/90 bg-clip-text'} truncate`}>
|
||||
{settings.company_name}
|
||||
</span>
|
||||
<span className="text-[8px] sm:text-[9px] md:text-[10px] text-[var(--luxury-gold)] tracking-[0.25em] uppercase font-light hidden sm:block">
|
||||
@@ -530,10 +542,10 @@ const Header: React.FC<HeaderProps> = ({
|
||||
<>
|
||||
<button
|
||||
onClick={() => openModal('login')}
|
||||
className="flex items-center space-x-2
|
||||
px-6 py-2.5 text-white/95
|
||||
className={`flex items-center space-x-2
|
||||
px-6 py-2.5 ${textClasses.primary}/95
|
||||
hover:text-[var(--luxury-gold)] transition-all duration-300
|
||||
font-light tracking-wider relative group overflow-hidden"
|
||||
font-light tracking-wider relative group overflow-hidden`}
|
||||
>
|
||||
<span className="absolute inset-0 border border-[rgba(var(--luxury-gold-rgb),0.2)] rounded-md opacity-0 group-hover:opacity-100 transition-all duration-300 group-hover:border-[rgba(var(--luxury-gold-rgb),0.5)]"></span>
|
||||
<span className="absolute inset-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.05)] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
||||
@@ -590,22 +602,26 @@ const Header: React.FC<HeaderProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<span className="font-light text-white/95 tracking-wider text-sm">
|
||||
<span className={`font-light ${textClasses.primary}/95 tracking-wider text-sm`}>
|
||||
{userInfo?.name}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isUserMenuOpen && (
|
||||
<div className="absolute right-0 mt-3
|
||||
w-56 bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f]
|
||||
rounded-lg shadow-[0_8px_32px_rgba(0,0,0,0.6)] py-3 border border-[rgba(var(--luxury-gold-rgb),0.2)]
|
||||
z-[9999] backdrop-blur-xl animate-fade-in"
|
||||
<div className={`absolute right-0 mt-3
|
||||
w-56 ${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-b from-white via-gray-50 to-white border-gray-200'
|
||||
: 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] border-[rgba(var(--luxury-gold-rgb),0.2)]'}
|
||||
rounded-lg ${theme.theme_layout_mode === 'light'
|
||||
? 'shadow-[0_8px_32px_rgba(0,0,0,0.15)]'
|
||||
: 'shadow-[0_8px_32px_rgba(0,0,0,0.6)]'} py-3 border
|
||||
z-[9999] backdrop-blur-xl animate-fade-in`}
|
||||
>
|
||||
<Link
|
||||
to="/profile"
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
className="flex items-center space-x-3
|
||||
px-5 py-3 text-white/95
|
||||
px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
@@ -620,7 +636,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
to="/dashboard"
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
className="flex items-center space-x-3
|
||||
px-5 py-3 text-white/95
|
||||
px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
@@ -633,7 +649,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
to="/favorites"
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
className="flex items-center space-x-3
|
||||
px-5 py-3 text-white/95
|
||||
px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
@@ -646,7 +662,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
to="/bookings"
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
className="flex items-center space-x-3
|
||||
px-5 py-3 text-white/95
|
||||
px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
@@ -659,7 +675,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
to="/loyalty"
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
className="flex items-center space-x-3
|
||||
px-5 py-3 text-white/95
|
||||
px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
@@ -672,7 +688,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
to="/group-bookings"
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
className="flex items-center space-x-3
|
||||
px-5 py-3 text-white/95
|
||||
px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
@@ -685,7 +701,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
to="/complaints"
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
className="flex items-center space-x-3
|
||||
px-5 py-3 text-white/95
|
||||
px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
@@ -698,7 +714,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
to="/guest-requests"
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
className="flex items-center space-x-3
|
||||
px-5 py-3 text-white/95
|
||||
px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
@@ -711,7 +727,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
to="/gdpr"
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
className="flex items-center space-x-3
|
||||
px-5 py-3 text-white/95
|
||||
px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
@@ -728,11 +744,11 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onClick={() =>
|
||||
setIsUserMenuOpen(false)
|
||||
}
|
||||
className="flex items-center
|
||||
space-x-3 px-5 py-3 text-white/95
|
||||
className={`flex items-center
|
||||
space-x-3 px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative`}
|
||||
>
|
||||
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
||||
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
||||
@@ -745,11 +761,11 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onClick={() =>
|
||||
setIsUserMenuOpen(false)
|
||||
}
|
||||
className="flex items-center
|
||||
space-x-3 px-5 py-3 text-white/95
|
||||
className={`flex items-center
|
||||
space-x-3 px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative`}
|
||||
>
|
||||
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
||||
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
||||
@@ -762,11 +778,11 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onClick={() =>
|
||||
setIsUserMenuOpen(false)
|
||||
}
|
||||
className="flex items-center
|
||||
space-x-3 px-5 py-3 text-white/95
|
||||
className={`flex items-center
|
||||
space-x-3 px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative`}
|
||||
>
|
||||
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
||||
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
||||
@@ -779,11 +795,11 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onClick={() =>
|
||||
setIsUserMenuOpen(false)
|
||||
}
|
||||
className="flex items-center
|
||||
space-x-3 px-5 py-3 text-white/95
|
||||
className={`flex items-center
|
||||
space-x-3 px-5 py-3 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||
hover:border-[var(--luxury-gold)] rounded-md group relative`}
|
||||
>
|
||||
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
||||
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
||||
|
||||
@@ -2,6 +2,8 @@ import React, { useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { getThemeTextClasses } from '../utils/themeUtils';
|
||||
|
||||
interface NavbarProps {
|
||||
isMobileMenuOpen: boolean;
|
||||
@@ -27,6 +29,8 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
renderMobileLinksOnly = false,
|
||||
mobileMenuContent
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||
const mobileMenuContainerRef = useRef<HTMLDivElement>(null);
|
||||
const mobileMenuDropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -88,11 +92,11 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
key={link.to}
|
||||
to={link.to}
|
||||
onClick={handleLinkClick}
|
||||
className="px-4 py-3 text-white/90
|
||||
className={`px-4 py-3 ${textClasses.primary}/90
|
||||
hover:bg-[var(--luxury-gold)]/10 hover:text-[var(--luxury-gold)]
|
||||
rounded-sm transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
@@ -109,9 +113,9 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
<Link
|
||||
key={link.to}
|
||||
to={link.to}
|
||||
className="text-white/95 hover:text-[var(--luxury-gold)]
|
||||
className={`${textClasses.primary}/95 hover:text-[var(--luxury-gold)]
|
||||
transition-all duration-300 font-light px-5 py-2.5
|
||||
relative group tracking-wider text-sm uppercase"
|
||||
relative group tracking-wider text-sm uppercase`}
|
||||
>
|
||||
<span className="relative z-10 transition-all duration-300 group-hover:tracking-widest">
|
||||
{link.label}
|
||||
@@ -148,7 +152,7 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
<>
|
||||
{/* Mobile Menu Backdrop */}
|
||||
<div
|
||||
className="md:hidden fixed inset-0 bg-black/70 backdrop-blur-sm"
|
||||
className={`md:hidden fixed inset-0 ${theme.theme_layout_mode === 'light' ? 'bg-black/40' : 'bg-black/70'} backdrop-blur-sm`}
|
||||
style={{
|
||||
top: '73px',
|
||||
zIndex: 99998,
|
||||
@@ -174,12 +178,14 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
{/* Mobile Menu Dropdown - Right side on mobile */}
|
||||
<div
|
||||
ref={mobileMenuDropdownRef}
|
||||
className="md:hidden fixed top-[73px] right-0 w-80 max-w-[85vw]
|
||||
bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f]
|
||||
className={`md:hidden fixed top-[73px] right-0 w-80 max-w-[85vw]
|
||||
${theme.theme_layout_mode === 'light'
|
||||
? 'bg-gradient-to-b from-white via-gray-50 to-white'
|
||||
: 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f]'}
|
||||
shadow-[0_8px_32px_rgba(0,0,0,0.9),_-4px_0_20px_rgba(var(--luxury-gold-rgb),0.2)]
|
||||
py-4 border-l-2 border-[rgba(var(--luxury-gold-rgb),0.4)]
|
||||
backdrop-blur-xl animate-fade-in
|
||||
overflow-y-auto h-[calc(100vh-73px)]"
|
||||
overflow-y-auto h-[calc(100vh-73px)]`}
|
||||
style={{
|
||||
zIndex: 99999,
|
||||
position: 'fixed',
|
||||
@@ -227,12 +233,12 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
// Ensure mouse events work
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="px-4 py-3.5 text-white/95
|
||||
className={`px-4 py-3.5 ${textClasses.primary}/95
|
||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
|
||||
rounded-md transition-all duration-300
|
||||
border-l-2 border-transparent
|
||||
hover:border-[var(--luxury-gold)] font-light tracking-wider text-sm uppercase
|
||||
group relative mx-2 cursor-pointer text-left w-full block"
|
||||
group relative mx-2 cursor-pointer text-left w-full block`}
|
||||
style={{
|
||||
touchAction: 'manipulation',
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
|
||||
@@ -17,6 +17,10 @@ type CompanySettings = {
|
||||
company_address: string;
|
||||
chat_working_hours_start: number;
|
||||
chat_working_hours_end: number;
|
||||
bank_name?: string;
|
||||
bank_account_number?: string;
|
||||
bank_account_holder?: string;
|
||||
bank_code?: string;
|
||||
};
|
||||
|
||||
type CompanySettingsContextValue = {
|
||||
@@ -35,6 +39,10 @@ const defaultSettings: CompanySettings = {
|
||||
company_address: '',
|
||||
chat_working_hours_start: 9,
|
||||
chat_working_hours_end: 17,
|
||||
bank_name: '',
|
||||
bank_account_number: '',
|
||||
bank_account_holder: '',
|
||||
bank_code: '',
|
||||
};
|
||||
|
||||
const CompanySettingsContext = createContext<CompanySettingsContextValue | undefined>(undefined);
|
||||
@@ -71,6 +79,10 @@ export const CompanySettingsProvider: React.FC<CompanySettingsProviderProps> = (
|
||||
company_address: response.data.company_address || defaultSettings.company_address,
|
||||
chat_working_hours_start: response.data.chat_working_hours_start || defaultSettings.chat_working_hours_start,
|
||||
chat_working_hours_end: response.data.chat_working_hours_end || defaultSettings.chat_working_hours_end,
|
||||
bank_name: response.data.bank_name || defaultSettings.bank_name,
|
||||
bank_account_number: response.data.bank_account_number || defaultSettings.bank_account_number,
|
||||
bank_account_holder: response.data.bank_account_holder || defaultSettings.bank_account_holder,
|
||||
bank_code: response.data.bank_code || defaultSettings.bank_code,
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -7,25 +7,28 @@ import React, {
|
||||
} from 'react';
|
||||
import { themeService } from '../../features/system/services/systemSettingsService';
|
||||
|
||||
type ThemeColors = {
|
||||
primary: string;
|
||||
primaryLight: string;
|
||||
primaryDark: string;
|
||||
primaryAccent: string;
|
||||
type ThemeLayoutMode = 'dark' | 'light';
|
||||
|
||||
type ThemeSettings = {
|
||||
theme_primary_color: string;
|
||||
theme_primary_light: string;
|
||||
theme_primary_dark: string;
|
||||
theme_primary_accent: string;
|
||||
theme_layout_mode: ThemeLayoutMode;
|
||||
};
|
||||
|
||||
type ThemeContextValue = {
|
||||
colors: ThemeColors;
|
||||
theme: ThemeSettings;
|
||||
isLoading: boolean;
|
||||
refreshTheme: () => Promise<void>;
|
||||
updateColors: (colors: Partial<ThemeColors>) => void;
|
||||
};
|
||||
|
||||
const defaultColors: ThemeColors = {
|
||||
primary: '#d4af37',
|
||||
primaryLight: '#f5d76e',
|
||||
primaryDark: '#c9a227',
|
||||
primaryAccent: '#e8c547',
|
||||
const defaultTheme: ThemeSettings = {
|
||||
theme_primary_color: '#d4af37',
|
||||
theme_primary_light: '#f5d76e',
|
||||
theme_primary_dark: '#c9a227',
|
||||
theme_primary_accent: '#e8c547',
|
||||
theme_layout_mode: 'dark',
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
||||
@@ -42,58 +45,8 @@ interface ThemeProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts hex color to RGB values
|
||||
*/
|
||||
const hexToRgb = (hex: string): string => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`
|
||||
: '212, 175, 55'; // Default gold RGB
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies theme colors as CSS variables to the document root
|
||||
*/
|
||||
const applyThemeColors = (colors: ThemeColors) => {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Apply CSS variables
|
||||
root.style.setProperty('--luxury-gold', colors.primary);
|
||||
root.style.setProperty('--luxury-gold-light', colors.primaryLight);
|
||||
root.style.setProperty('--luxury-gold-dark', colors.primaryDark);
|
||||
root.style.setProperty('--luxury-gold-accent', colors.primaryAccent);
|
||||
|
||||
// Add RGB versions for rgba() usage
|
||||
root.style.setProperty('--luxury-gold-rgb', hexToRgb(colors.primary));
|
||||
root.style.setProperty('--luxury-gold-light-rgb', hexToRgb(colors.primaryLight));
|
||||
root.style.setProperty('--luxury-gold-dark-rgb', hexToRgb(colors.primaryDark));
|
||||
root.style.setProperty('--luxury-gold-accent-rgb', hexToRgb(colors.primaryAccent));
|
||||
|
||||
// Also update gradient variables
|
||||
root.style.setProperty(
|
||||
'--gradient-gold',
|
||||
`linear-gradient(135deg, ${colors.primary} 0%, ${colors.primaryLight} 100%)`
|
||||
);
|
||||
root.style.setProperty(
|
||||
'--gradient-gold-dark',
|
||||
`linear-gradient(135deg, ${colors.primaryDark} 0%, ${colors.primary} 100%)`
|
||||
);
|
||||
|
||||
// Update shadow variables with proper opacity
|
||||
const primaryRgb = hexToRgb(colors.primary);
|
||||
root.style.setProperty(
|
||||
'--shadow-luxury',
|
||||
`0 4px 20px rgba(${primaryRgb}, 0.15)`
|
||||
);
|
||||
root.style.setProperty(
|
||||
'--shadow-luxury-gold',
|
||||
`0 8px 30px rgba(${primaryRgb}, 0.25)`
|
||||
);
|
||||
};
|
||||
|
||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
const [colors, setColors] = useState<ThemeColors>(defaultColors);
|
||||
const [theme, setTheme] = useState<ThemeSettings>(defaultTheme);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
const loadTheme = async () => {
|
||||
@@ -101,20 +54,34 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
setIsLoading(true);
|
||||
const response = await themeService.getThemeSettings();
|
||||
if (response.data) {
|
||||
const newColors: ThemeColors = {
|
||||
primary: response.data.theme_primary_color || defaultColors.primary,
|
||||
primaryLight: response.data.theme_primary_light || defaultColors.primaryLight,
|
||||
primaryDark: response.data.theme_primary_dark || defaultColors.primaryDark,
|
||||
primaryAccent: response.data.theme_primary_accent || defaultColors.primaryAccent,
|
||||
const themeData: ThemeSettings = {
|
||||
theme_primary_color: response.data.theme_primary_color || defaultTheme.theme_primary_color,
|
||||
theme_primary_light: response.data.theme_primary_light || defaultTheme.theme_primary_light,
|
||||
theme_primary_dark: response.data.theme_primary_dark || defaultTheme.theme_primary_dark,
|
||||
theme_primary_accent: response.data.theme_primary_accent || defaultTheme.theme_primary_accent,
|
||||
theme_layout_mode: (response.data.theme_layout_mode === 'light' ? 'light' : 'dark') as ThemeLayoutMode,
|
||||
};
|
||||
setColors(newColors);
|
||||
applyThemeColors(newColors);
|
||||
setTheme(themeData);
|
||||
|
||||
// Apply CSS variables for colors
|
||||
document.documentElement.style.setProperty('--luxury-gold', themeData.theme_primary_color);
|
||||
document.documentElement.style.setProperty('--luxury-gold-light', themeData.theme_primary_light);
|
||||
document.documentElement.style.setProperty('--luxury-gold-dark', themeData.theme_primary_dark);
|
||||
document.documentElement.style.setProperty('--luxury-gold-accent', themeData.theme_primary_accent);
|
||||
|
||||
// Apply layout mode class to body
|
||||
document.body.classList.remove('theme-dark', 'theme-light');
|
||||
document.body.classList.add(`theme-${themeData.theme_layout_mode}`);
|
||||
document.documentElement.classList.remove('theme-dark', 'theme-light');
|
||||
document.documentElement.classList.add(`theme-${themeData.theme_layout_mode}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading theme settings:', error);
|
||||
// Apply default colors on error
|
||||
applyThemeColors(defaultColors);
|
||||
setColors(defaultColors);
|
||||
// Apply defaults
|
||||
document.body.classList.remove('theme-dark', 'theme-light');
|
||||
document.body.classList.add('theme-dark');
|
||||
document.documentElement.classList.remove('theme-dark', 'theme-light');
|
||||
document.documentElement.classList.add('theme-dark');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -122,12 +89,12 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
|
||||
useEffect(() => {
|
||||
loadTheme();
|
||||
|
||||
|
||||
// Listen for theme refresh events
|
||||
const handleRefresh = () => {
|
||||
loadTheme();
|
||||
};
|
||||
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('refreshTheme', handleRefresh);
|
||||
return () => {
|
||||
@@ -140,23 +107,15 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
await loadTheme();
|
||||
};
|
||||
|
||||
const updateColors = (newColors: Partial<ThemeColors>) => {
|
||||
const updatedColors = { ...colors, ...newColors };
|
||||
setColors(updatedColors);
|
||||
applyThemeColors(updatedColors);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
colors,
|
||||
theme,
|
||||
isLoading,
|
||||
refreshTheme,
|
||||
updateColors,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ export const DATE_FORMATS = {
|
||||
} as const;
|
||||
|
||||
export const CURRENCY = {
|
||||
VND: 'VND',
|
||||
USD: 'USD',
|
||||
EUR: 'EUR',
|
||||
GBP: 'GBP',
|
||||
} as const;
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
|
||||
@@ -43,9 +43,7 @@ export const formatCurrency = (
|
||||
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
|
||||
if (isNaN(numAmount)) {
|
||||
|
||||
if (currency === 'VND') return '0 ₫';
|
||||
return `0 ${currency || 'VND'}`;
|
||||
return `0 ${currency || 'USD'}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,16 +65,7 @@ export const formatCurrency = (
|
||||
}
|
||||
}
|
||||
|
||||
currencyToUse = currencyToUse || 'VND';
|
||||
|
||||
if (currencyToUse === 'VND') {
|
||||
return new Intl.NumberFormat('vi-VN', {
|
||||
style: 'currency',
|
||||
currency: 'VND',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(numAmount);
|
||||
}
|
||||
currencyToUse = currencyToUse || 'USD';
|
||||
|
||||
|
||||
const localeMap: Record<string, string> = {
|
||||
@@ -242,3 +231,20 @@ export const truncateText = (
|
||||
return text.slice(0, maxLength - suffix.length) + suffix;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format working hours from 24-hour format to 12-hour format
|
||||
* @param startHour - Start hour in 24-hour format (0-23)
|
||||
* @param endHour - End hour in 24-hour format (0-23)
|
||||
* @returns Formatted string like "9:00 AM - 5:00 PM"
|
||||
*/
|
||||
export const formatWorkingHours = (startHour: number, endHour: number): string => {
|
||||
const formatHour = (hour: number): string => {
|
||||
if (hour === 0) return '12:00 AM';
|
||||
if (hour < 12) return `${hour}:00 AM`;
|
||||
if (hour === 12) return '12:00 PM';
|
||||
return `${hour - 12}:00 PM`;
|
||||
};
|
||||
|
||||
return `${formatHour(startHour)} - ${formatHour(endHour)}`;
|
||||
};
|
||||
|
||||
|
||||
73
Frontend/src/shared/utils/themeUtils.ts
Normal file
73
Frontend/src/shared/utils/themeUtils.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Theme utility functions for applying theme-aware classes
|
||||
*/
|
||||
|
||||
export type ThemeLayoutMode = 'dark' | 'light';
|
||||
|
||||
/**
|
||||
* Get background classes based on theme layout mode
|
||||
*/
|
||||
export const getThemeBackgroundClasses = (mode: ThemeLayoutMode): string => {
|
||||
if (mode === 'light') {
|
||||
return 'bg-gradient-to-b from-gray-50 via-white to-gray-50';
|
||||
}
|
||||
// Dark mode (default)
|
||||
return 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f]';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get hero section background classes based on theme layout mode
|
||||
*/
|
||||
export const getThemeHeroBackgroundClasses = (mode: ThemeLayoutMode): string => {
|
||||
if (mode === 'light') {
|
||||
return 'bg-gradient-to-br from-gray-50 via-white to-gray-50 border-b border-gray-200';
|
||||
}
|
||||
// Dark mode (default)
|
||||
return 'bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get text color classes based on theme layout mode
|
||||
*/
|
||||
export const getThemeTextClasses = (mode: ThemeLayoutMode): {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
muted: string;
|
||||
} => {
|
||||
if (mode === 'light') {
|
||||
return {
|
||||
primary: 'text-gray-900',
|
||||
secondary: 'text-gray-700',
|
||||
muted: 'text-gray-500',
|
||||
};
|
||||
}
|
||||
// Dark mode (default)
|
||||
return {
|
||||
primary: 'text-white',
|
||||
secondary: 'text-gray-300',
|
||||
muted: 'text-gray-400',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get card/container background classes based on theme layout mode
|
||||
*/
|
||||
export const getThemeCardClasses = (mode: ThemeLayoutMode): string => {
|
||||
if (mode === 'light') {
|
||||
return 'bg-white border border-gray-200';
|
||||
}
|
||||
// Dark mode (default)
|
||||
return 'bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] border border-[var(--luxury-gold)]/30';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get input/field background classes based on theme layout mode
|
||||
*/
|
||||
export const getThemeInputClasses = (mode: ThemeLayoutMode): string => {
|
||||
if (mode === 'light') {
|
||||
return 'bg-white border-gray-300 text-gray-900';
|
||||
}
|
||||
// Dark mode (default)
|
||||
return 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-gray-700 text-white';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user