This commit is contained in:
Iliyan Angelov
2025-12-09 00:14:21 +02:00
parent b818d645a9
commit e43a95eafb
43 changed files with 2070 additions and 772 deletions

View File

@@ -1435,6 +1435,10 @@ class UpdateCompanySettingsRequest(BaseModel):
tax_rate: Optional[float] = None tax_rate: Optional[float] = None
chat_working_hours_start: Optional[int] = None chat_working_hours_start: Optional[int] = None
chat_working_hours_end: Optional[int] = None chat_working_hours_end: Optional[int] = None
bank_name: Optional[str] = None
bank_account_number: Optional[str] = None
bank_account_holder: Optional[str] = None
bank_code: Optional[str] = None
@router.get("/company") @router.get("/company")
async def get_company_settings( async def get_company_settings(
@@ -1452,6 +1456,10 @@ async def get_company_settings(
"tax_rate", "tax_rate",
"chat_working_hours_start", "chat_working_hours_start",
"chat_working_hours_end", "chat_working_hours_end",
"bank_name",
"bank_account_number",
"bank_account_holder",
"bank_code",
] ]
settings_dict = {} settings_dict = {}
@@ -1488,6 +1496,10 @@ async def get_company_settings(
"tax_rate": float(settings_dict.get("tax_rate", 0)) if settings_dict.get("tax_rate") else 0.0, "tax_rate": float(settings_dict.get("tax_rate", 0)) if settings_dict.get("tax_rate") else 0.0,
"chat_working_hours_start": int(settings_dict.get("chat_working_hours_start", 9)) if settings_dict.get("chat_working_hours_start") else 9, "chat_working_hours_start": int(settings_dict.get("chat_working_hours_start", 9)) if settings_dict.get("chat_working_hours_start") else 9,
"chat_working_hours_end": int(settings_dict.get("chat_working_hours_end", 17)) if settings_dict.get("chat_working_hours_end") else 17, "chat_working_hours_end": int(settings_dict.get("chat_working_hours_end", 17)) if settings_dict.get("chat_working_hours_end") else 17,
"bank_name": settings_dict.get("bank_name", ""),
"bank_account_number": settings_dict.get("bank_account_number", ""),
"bank_account_holder": settings_dict.get("bank_account_holder", ""),
"bank_code": settings_dict.get("bank_code", ""),
"updated_at": updated_at, "updated_at": updated_at,
"updated_by": updated_by, "updated_by": updated_by,
} }
@@ -1533,6 +1545,14 @@ async def update_company_settings(
db_settings["chat_working_hours_start"] = str(request_data.chat_working_hours_start) db_settings["chat_working_hours_start"] = str(request_data.chat_working_hours_start)
if request_data.chat_working_hours_end is not None: if request_data.chat_working_hours_end is not None:
db_settings["chat_working_hours_end"] = str(request_data.chat_working_hours_end) db_settings["chat_working_hours_end"] = str(request_data.chat_working_hours_end)
if request_data.bank_name is not None:
db_settings["bank_name"] = request_data.bank_name
if request_data.bank_account_number is not None:
db_settings["bank_account_number"] = request_data.bank_account_number
if request_data.bank_account_holder is not None:
db_settings["bank_account_holder"] = request_data.bank_account_holder
if request_data.bank_code is not None:
db_settings["bank_code"] = request_data.bank_code
for key, value in db_settings.items(): for key, value in db_settings.items():
@@ -1581,7 +1601,7 @@ async def update_company_settings(
updated_settings = {} updated_settings = {}
for key in ["company_name", "company_tagline", "company_logo_url", "company_favicon_url", "company_phone", "company_email", "company_address", "tax_rate", "chat_working_hours_start", "chat_working_hours_end"]: for key in ["company_name", "company_tagline", "company_logo_url", "company_favicon_url", "company_phone", "company_email", "company_address", "tax_rate", "chat_working_hours_start", "chat_working_hours_end", "bank_name", "bank_account_number", "bank_account_holder", "bank_code"]:
setting = db.query(SystemSettings).filter( setting = db.query(SystemSettings).filter(
SystemSettings.key == key SystemSettings.key == key
).first() ).first()
@@ -1619,6 +1639,10 @@ async def update_company_settings(
"tax_rate": float(updated_settings.get("tax_rate", 0)) if updated_settings.get("tax_rate") else 0.0, "tax_rate": float(updated_settings.get("tax_rate", 0)) if updated_settings.get("tax_rate") else 0.0,
"chat_working_hours_start": int(updated_settings.get("chat_working_hours_start", 9)) if updated_settings.get("chat_working_hours_start") else 9, "chat_working_hours_start": int(updated_settings.get("chat_working_hours_start", 9)) if updated_settings.get("chat_working_hours_start") else 9,
"chat_working_hours_end": int(updated_settings.get("chat_working_hours_end", 17)) if updated_settings.get("chat_working_hours_end") else 17, "chat_working_hours_end": int(updated_settings.get("chat_working_hours_end", 17)) if updated_settings.get("chat_working_hours_end") else 17,
"bank_name": updated_settings.get("bank_name", ""),
"bank_account_number": updated_settings.get("bank_account_number", ""),
"bank_account_holder": updated_settings.get("bank_account_holder", ""),
"bank_code": updated_settings.get("bank_code", ""),
"updated_at": updated_at, "updated_at": updated_at,
"updated_by": updated_by, "updated_by": updated_by,
} }
@@ -1851,6 +1875,7 @@ class UpdateThemeSettingsRequest(BaseModel):
theme_primary_light: Optional[str] = None theme_primary_light: Optional[str] = None
theme_primary_dark: Optional[str] = None theme_primary_dark: Optional[str] = None
theme_primary_accent: Optional[str] = None theme_primary_accent: Optional[str] = None
theme_layout_mode: Optional[str] = None # 'dark' or 'light'
@router.get("/theme") @router.get("/theme")
async def get_theme_settings( async def get_theme_settings(
@@ -1863,6 +1888,7 @@ async def get_theme_settings(
"theme_primary_light", "theme_primary_light",
"theme_primary_dark", "theme_primary_dark",
"theme_primary_accent", "theme_primary_accent",
"theme_layout_mode",
] ]
settings_dict = {} settings_dict = {}
@@ -1893,6 +1919,7 @@ async def get_theme_settings(
"theme_primary_light": settings_dict.get("theme_primary_light", "#f5d76e"), "theme_primary_light": settings_dict.get("theme_primary_light", "#f5d76e"),
"theme_primary_dark": settings_dict.get("theme_primary_dark", "#c9a227"), "theme_primary_dark": settings_dict.get("theme_primary_dark", "#c9a227"),
"theme_primary_accent": settings_dict.get("theme_primary_accent", "#e8c547"), "theme_primary_accent": settings_dict.get("theme_primary_accent", "#e8c547"),
"theme_layout_mode": settings_dict.get("theme_layout_mode", "dark"),
"updated_at": updated_at, "updated_at": updated_at,
"updated_by": updated_by, "updated_by": updated_by,
} }
@@ -1953,6 +1980,14 @@ async def update_theme_settings(
) )
db_settings["theme_primary_accent"] = request_data.theme_primary_accent db_settings["theme_primary_accent"] = request_data.theme_primary_accent
if request_data.theme_layout_mode is not None:
if request_data.theme_layout_mode not in ["dark", "light"]:
raise HTTPException(
status_code=400,
detail="Invalid theme_layout_mode. Must be 'dark' or 'light'"
)
db_settings["theme_layout_mode"] = request_data.theme_layout_mode
# Update or create settings # Update or create settings
for key, value in db_settings.items(): for key, value in db_settings.items():
setting = db.query(SystemSettings).filter( setting = db.query(SystemSettings).filter(
@@ -1975,7 +2010,7 @@ async def update_theme_settings(
# Get updated settings # Get updated settings
updated_settings = {} updated_settings = {}
for key in ["theme_primary_color", "theme_primary_light", "theme_primary_dark", "theme_primary_accent"]: for key in ["theme_primary_color", "theme_primary_light", "theme_primary_dark", "theme_primary_accent", "theme_layout_mode"]:
setting = db.query(SystemSettings).filter( setting = db.query(SystemSettings).filter(
SystemSettings.key == key SystemSettings.key == key
).first() ).first()
@@ -1988,6 +2023,7 @@ async def update_theme_settings(
"theme_primary_light": "#f5d76e", "theme_primary_light": "#f5d76e",
"theme_primary_dark": "#c9a227", "theme_primary_dark": "#c9a227",
"theme_primary_accent": "#e8c547", "theme_primary_accent": "#e8c547",
"theme_layout_mode": "dark",
} }
updated_settings[key] = defaults.get(key) updated_settings[key] = defaults.get(key)
@@ -2009,6 +2045,7 @@ async def update_theme_settings(
"theme_primary_light": updated_settings.get("theme_primary_light", "#f5d76e"), "theme_primary_light": updated_settings.get("theme_primary_light", "#f5d76e"),
"theme_primary_dark": updated_settings.get("theme_primary_dark", "#c9a227"), "theme_primary_dark": updated_settings.get("theme_primary_dark", "#c9a227"),
"theme_primary_accent": updated_settings.get("theme_primary_accent", "#e8c547"), "theme_primary_accent": updated_settings.get("theme_primary_accent", "#e8c547"),
"theme_layout_mode": updated_settings.get("theme_layout_mode", "dark"),
"updated_at": updated_at, "updated_at": updated_at,
"updated_by": updated_by, "updated_by": updated_by,
} }

View File

@@ -307,22 +307,24 @@ export const notifyPayment = async (
export const generateQRCode = ( export const generateQRCode = (
bookingNumber: string, bookingNumber: string,
amount: number amount: number,
bankCode?: string,
accountNumber?: string,
accountName?: string
): 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; 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 = const qrUrl =
`https://img.vietqr.io/image/${bankCode}-${accountNumber}-compact2.jpg?` + `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=` +
`amount=${amount}&` + encodeURIComponent(`Bank: ${bankCode}, Account: ${accountNumber}, Name: ${accountName}, Amount: ${amount}, Reference: ${transferContent}`);
`addInfo=${encodeURIComponent(transferContent)}&` +
`accountName=${encodeURIComponent(accountName)}`;
return qrUrl; return qrUrl;
}; };

View File

@@ -6,20 +6,26 @@ import {
Phone, Phone,
Mail, Mail,
Linkedin, Linkedin,
Twitter Twitter,
Clock
} from 'lucide-react'; } from 'lucide-react';
import * as LucideIcons from 'lucide-react'; import * as LucideIcons from 'lucide-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService'; import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService'; import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer'; import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
import { formatWorkingHours } from '../../../shared/utils/format';
import { getThemeTextClasses } from '../../../shared/utils/themeUtils';
const AboutPage: React.FC = () => { const AboutPage: React.FC = () => {
const { settings } = useCompanySettings(); const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null); const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [apiError, setApiError] = useState(false); const [apiError, setApiError] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
useEffect(() => { useEffect(() => {
const fetchPageContent = async () => { const fetchPageContent = async () => {
@@ -194,7 +200,9 @@ const AboutPage: React.FC = () => {
<div className="relative"> <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="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"> <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> </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> <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.bio && <p className="text-gray-600 text-sm mb-6 leading-relaxed font-light">{member.bio}</p>}
{member.social_links && ( {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 && ( {member.social_links.linkedin && (
<a <a
href={member.social_links.linkedin} href={member.social_links.linkedin}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" /> <Linkedin className="w-5 h-5" />
</a> </a>
@@ -461,7 +471,9 @@ const AboutPage: React.FC = () => {
href={member.social_links.twitter} href={member.social_links.twitter}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" /> <Twitter className="w-5 h-5" />
</a> </a>
@@ -614,7 +626,9 @@ const AboutPage: React.FC = () => {
{displayAddress && ( {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="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"> <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> </div>
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300"> <h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
Address Address
@@ -633,7 +647,9 @@ const AboutPage: React.FC = () => {
{displayPhone && ( {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="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"> <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> </div>
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300"> <h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
Phone Phone
@@ -648,7 +664,9 @@ const AboutPage: React.FC = () => {
{displayEmail && ( {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="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"> <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> </div>
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300"> <h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
Email Email
@@ -660,6 +678,19 @@ const AboutPage: React.FC = () => {
</p> </p>
</div> </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>
)} )}
<div className="text-center"> <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" 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> <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> <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> </Link>
</div> </div>

View File

@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService'; import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService'; import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading'; import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer'; import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
const AccessibilityPage: React.FC = () => { const AccessibilityPage: React.FC = () => {
const { settings } = useCompanySettings(); const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null); const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true); 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(() => { useEffect(() => {
const fetchPageContent = async () => { const fetchPageContent = async () => {
try { try {
@@ -26,22 +33,26 @@ const AccessibilityPage: React.FC = () => {
const tempDiv = document.createElement('div'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent; 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('*'); const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => { allElements.forEach((el) => {
const htmlEl = el as HTMLElement; const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase(); const tagName = htmlEl.tagName.toLowerCase();
const currentColor = htmlEl.style.color;
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { // Override inline colors to use theme-aware colors
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = '#ffffff'; htmlEl.style.color = headingColor;
} else if (['strong', 'b'].includes(tagName)) { } else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = '#d4af37'; htmlEl.style.color = accentColor;
} else if (tagName === 'a') { } else if (tagName === 'a') {
htmlEl.style.color = '#d4af37'; htmlEl.style.color = accentColor;
} else { } else {
htmlEl.style.color = '#d1d5db'; htmlEl.style.color = bodyColor;
}
} }
}); });
@@ -83,7 +94,7 @@ const AccessibilityPage: React.FC = () => {
if (!pageContent) { if (!pageContent) {
return ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)', marginRight: 'calc(50% - 50vw)',
@@ -95,8 +106,8 @@ const AccessibilityPage: React.FC = () => {
> >
<div className="text-center"> <div className="text-center">
<Accessibility className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" /> <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> <h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Accessibility</h1>
<p className="text-gray-400">This page is currently unavailable.</p> <p className={textClasses.muted}>This page is currently unavailable.</p>
<Link <Link
to="/" to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300" 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 ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: '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"> <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)]" /> <Accessibility className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div> </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'} {pageContent.title || 'Accessibility'}
</h1> </h1>
{pageContent.subtitle && ( {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} {pageContent.subtitle}
</p> </p>
)} )}
</div> </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 <div
className="prose prose-invert prose-lg max-w-none text-gray-300 className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold 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-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 ${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-ul:text-gray-300 prose-ul:font-light prose-ul:my-4 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-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-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline 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 style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
style={{ color: '#d1d5db' }}
dangerouslySetInnerHTML={createSanitizedHtml( 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> </div>
{settings.company_email && ( {settings.company_email && (
<div className="mt-8 text-center"> <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{' '} For accessibility inquiries, contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline"> <a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email} {settings.company_email}

View File

@@ -4,14 +4,21 @@ import { Calendar, User, Tag, ArrowLeft, Eye, Share2, ArrowRight } from 'lucide-
import blogService, { BlogPost } from '../services/blogService'; import blogService, { BlogPost } from '../services/blogService';
import Loading from '../../../shared/components/Loading'; import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer'; 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 BlogDetailPage: React.FC = () => {
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { theme } = useTheme();
const [post, setPost] = useState<BlogPost | null>(null); const [post, setPost] = useState<BlogPost | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [relatedPosts, setRelatedPosts] = useState<BlogPost[]>([]); 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(() => { useEffect(() => {
if (slug) { if (slug) {
fetchPost(); fetchPost();
@@ -108,9 +115,9 @@ const BlogDetailPage: React.FC = () => {
if (!post) { if (!post) {
return ( 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"> <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"> <Link to="/blog" className="text-[var(--luxury-gold)] hover:underline">
Back to Blog Back to Blog
</Link> </Link>
@@ -120,7 +127,7 @@ const BlogDetailPage: React.FC = () => {
} }
return ( 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 */} {/* Hero Section with Featured Image */}
{post.featured_image && ( {post.featured_image && (
<div className="relative w-full h-64 sm:h-80 lg:h-96 overflow-hidden"> <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 */} {/* Back Button */}
<Link <Link
to="/blog" 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" /> <ArrowLeft className="w-4 h-4" />
<span>Back to Blog</span> <span>Back to Blog</span>
@@ -173,7 +180,7 @@ const BlogDetailPage: React.FC = () => {
</p> </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 && ( {post.published_at && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
@@ -201,19 +208,18 @@ const BlogDetailPage: React.FC = () => {
</div> </div>
{/* Article Content */} {/* Article Content */}
<article className="prose prose-invert prose-lg max-w-none mb-12 <article className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none mb-12
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold 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-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-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-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-ul:font-light prose-ul:my-6
prose-ol:text-gray-300 prose-ol:font-light prose-ol:my-6 prose-ol:font-light prose-ol:my-6
prose-li:text-gray-300 prose-li:mb-2 prose-li:mb-2
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium 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-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
prose-img:rounded-xl prose-img:shadow-2xl prose-img:rounded-xl prose-img:shadow-2xl`}
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white style={{ color: theme.theme_layout_mode === 'light' ? '#374151' : '#d1d5db' }}
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
> >
<div <div
dangerouslySetInnerHTML={createSanitizedHtml( dangerouslySetInnerHTML={createSanitizedHtml(
@@ -245,7 +251,7 @@ const BlogDetailPage: React.FC = () => {
</h2> </h2>
)} )}
{section.content && ( {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} {section.content}
</p> </p>
)} )}
@@ -277,8 +283,8 @@ const BlogDetailPage: React.FC = () => {
<div className="rounded-3xl overflow-hidden border-2 border-[var(--luxury-gold)]/20 shadow-2xl"> <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" /> <img src={section.image} alt={section.title || 'Blog image'} className="w-full h-auto" />
{section.title && ( {section.title && (
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] px-6 py-4 border-t border-[var(--luxury-gold)]/20"> <div className={`${cardClasses} 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> <p className={`${textClasses.muted} text-sm font-light italic text-center`}>{section.title}</p>
</div> </div>
)} )}
</div> </div>
@@ -373,7 +379,7 @@ const BlogDetailPage: React.FC = () => {
{/* Related Posts */} {/* Related Posts */}
{relatedPosts.length > 0 && ( {relatedPosts.length > 0 && (
<div className="mt-16 pt-12 border-t border-[var(--luxury-gold)]/20"> <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"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{relatedPosts.map((relatedPost) => ( {relatedPosts.map((relatedPost) => (
<Link <Link
@@ -391,11 +397,11 @@ const BlogDetailPage: React.FC = () => {
</div> </div>
)} )}
<div className="p-4"> <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} {relatedPost.title}
</h3> </h3>
{relatedPost.excerpt && ( {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} {relatedPost.excerpt}
</p> </p>
)} )}

View File

@@ -3,9 +3,12 @@ import { Link } from 'react-router-dom';
import { Calendar, User, Tag, Search, ArrowRight, Eye, Sparkles, BookOpen } from 'lucide-react'; import { Calendar, User, Tag, Search, ArrowRight, Eye, Sparkles, BookOpen } from 'lucide-react';
import blogService, { BlogPost } from '../services/blogService'; import blogService, { BlogPost } from '../services/blogService';
import Loading from '../../../shared/components/Loading'; 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'; import Pagination from '../../../shared/components/Pagination';
const BlogPage: React.FC = () => { const BlogPage: React.FC = () => {
const { theme } = useTheme();
const [posts, setPosts] = useState<BlogPost[]>([]); const [posts, setPosts] = useState<BlogPost[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@@ -67,10 +70,16 @@ const BlogPage: React.FC = () => {
return <Loading />; 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 ( 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 */} {/* 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 */} {/* Background Effects */}
<div className="absolute inset-0 opacity-10"> <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> <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="flex justify-center mb-2 sm:mb-3 md:mb-4">
<div className="relative group"> <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="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" /> <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> </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"> <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 Our Blog
</span> </span>
</h1> </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> <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 Discover stories, insights, and updates from our luxury hotel
</p> </p>
<div className="mt-4 flex items-center justify-center gap-2 text-[var(--luxury-gold)]/60"> <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); setSearchTerm(e.target.value);
setCurrentPage(1); 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>
</div> </div>
@@ -139,7 +150,7 @@ const BlogPage: React.FC = () => {
{/* Results Count */} {/* Results Count */}
{!loading && total > 0 && ( {!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 Showing {posts.length} of {total} posts
</div> </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"> <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)]" /> <BookOpen className="w-10 h-10 text-[var(--luxury-gold)]" />
</div> </div>
<p className="text-gray-400 text-xl font-light">No blog posts found</p> <p className={`${textClasses.muted} 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-sm mt-2`}>Try adjusting your search or filters</p>
</div> </div>
) : ( ) : (
<> <>
@@ -160,7 +171,7 @@ const BlogPage: React.FC = () => {
<Link <Link
key={post.id} key={post.id}
to={`/blog/${post.slug}`} 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` }} style={{ animationDelay: `${index * 50}ms` }}
> >
{/* Premium Glow Effects */} {/* Premium Glow Effects */}
@@ -204,15 +215,15 @@ const BlogPage: React.FC = () => {
))} ))}
</div> </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} {post.title}
</h2> </h2>
{post.excerpt && ( {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} {post.excerpt}
</p> </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"> <div className="flex items-center gap-4">
{post.published_at && ( {post.published_at && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -259,10 +270,10 @@ const BlogPage: React.FC = () => {
<div className="lg:col-span-3"> <div className="lg:col-span-3">
{allTags.length > 0 && ( {allTags.length > 0 && (
<div className="sticky top-8"> <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="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> <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>
<div className="space-y-3"> <div className="space-y-3">
<button <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 ${ 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 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-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"> <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 ${ 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 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-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"> <span className="relative z-10 flex items-center gap-2">

View File

@@ -5,12 +5,19 @@ import pageContentService, { PageContent } from '../services/pageContentService'
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import Loading from '../../../shared/components/Loading'; import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer'; 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 CancellationPolicyPage: React.FC = () => {
const { settings } = useCompanySettings(); const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null); const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true); 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(() => { useEffect(() => {
const fetchPageContent = async () => { const fetchPageContent = async () => {
try { try {
@@ -26,22 +33,27 @@ const CancellationPolicyPage: React.FC = () => {
const tempDiv = document.createElement('div'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent; 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('*'); const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => { allElements.forEach((el) => {
const htmlEl = el as HTMLElement; const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase(); const tagName = htmlEl.tagName.toLowerCase();
const currentColor = htmlEl.style.color; const currentColor = htmlEl.style.color;
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { // Override inline colors to use theme-aware colors
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = '#ffffff'; htmlEl.style.color = headingColor;
} else if (['strong', 'b'].includes(tagName)) { } else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = '#d4af37'; htmlEl.style.color = accentColor;
} else if (tagName === 'a') { } else if (tagName === 'a') {
htmlEl.style.color = '#d4af37'; htmlEl.style.color = accentColor;
} else { } else {
htmlEl.style.color = '#d1d5db'; htmlEl.style.color = bodyColor;
}
} }
}); });
@@ -83,7 +95,7 @@ const CancellationPolicyPage: React.FC = () => {
if (!pageContent) { if (!pageContent) {
return ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)', marginRight: 'calc(50% - 50vw)',
@@ -95,8 +107,8 @@ const CancellationPolicyPage: React.FC = () => {
> >
<div className="text-center"> <div className="text-center">
<XCircle className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" /> <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> <h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Cancellation Policy</h1>
<p className="text-gray-400">This page is currently unavailable.</p> <p className={textClasses.muted}>This page is currently unavailable.</p>
<Link <Link
to="/" to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300" 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 ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: '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"> <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)]" /> <XCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div> </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'} {pageContent.title || 'Cancellation Policy'}
</h1> </h1>
{pageContent.subtitle && ( {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} {pageContent.subtitle}
</p> </p>
)} )}
</div> </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 <div
className="prose prose-invert prose-lg max-w-none text-gray-300 className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold 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-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 ${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-ul:text-gray-300 prose-ul:font-light prose-ul:my-4 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-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-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline 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 style={{
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]" color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db',
style={{ color: '#d1d5db' }} }}
dangerouslySetInnerHTML={createSanitizedHtml( 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> </div>
{settings.company_email && ( {settings.company_email && (
<div className="mt-8 text-center"> <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{' '} For questions about cancellations, contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline"> <a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email} {settings.company_email}

View File

@@ -1,18 +1,41 @@
import React, { useState, useEffect } from 'react'; 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 { submitContactForm } from '../services/contactService';
import pageContentService from '../services/pageContentService'; import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService'; import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Recaptcha from '../../../shared/components/Recaptcha'; import Recaptcha from '../../../shared/components/Recaptcha';
import { recaptchaService } from '../../../features/system/services/systemSettingsService'; import { recaptchaService } from '../../../features/system/services/systemSettingsService';
import ChatWidget from '../../notifications/components/ChatWidget'; import ChatWidget from '../../notifications/components/ChatWidget';
import { useAntibotForm } from '../../../features/auth/hooks/useAntibotForm'; import { useAntibotForm } from '../../../features/auth/hooks/useAntibotForm';
import HoneypotField from '../../../shared/components/HoneypotField'; 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 ContactPage: React.FC = () => {
const { settings } = useCompanySettings(); const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null); const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
@@ -153,9 +176,9 @@ const ContactPage: React.FC = () => {
}, []); }, []);
const displayPhone = settings.company_phone || 'Available 24/7 for your convenience'; const displayPhone = settings.company_phone || null;
const displayEmail = settings.company_email || "We'll respond within 24 hours"; const displayEmail = settings.company_email || null;
const displayAddress = settings.company_address || 'Visit us at our hotel reception'; const displayAddress = settings.company_address || null;
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target; 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 ( 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 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> <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="flex justify-center mb-2 sm:mb-3 md:mb-4">
<div className="relative group"> <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="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`}>
<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" /> {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> </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"> <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-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'} {pageContent?.title || 'Contact Us'}
</span> </span>
</h1> </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> <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."} {pageContent?.subtitle || pageContent?.description || "Experience the pinnacle of hospitality. We're here to make your stay extraordinary."}
</p> </p>
</div> </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="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="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 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 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="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="relative z-10">
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8"> <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> <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 <h2 className={`text-xl sm:text-2xl md:text-3xl font-serif font-semibold
text-white tracking-tight"> ${textClasses.primary} tracking-tight`}>
Get in Touch Get in Touch
</h2> </h2>
</div> </div>
@@ -231,11 +264,13 @@ const ContactPage: React.FC = () => {
<div className="space-y-5 sm:space-y-6 md:space-y-7"> <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="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"> <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>
<div> <div>
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Email</h3> <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} {displayEmail}
</a> </a>
</div> </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="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"> <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>
<div> <div>
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Phone</h3> <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} {displayPhone}
</a> </a>
</div> </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="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"> <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>
<div> <div>
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Location</h3> <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} {displayAddress}
</p> </p>
</div> </div>
</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> </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"> <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."} {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> </p>
</div> </div>
@@ -299,10 +352,10 @@ const ContactPage: React.FC = () => {
{} {}
<div className="lg:col-span-8"> <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 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 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 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> <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="relative z-10">
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8"> <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> <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 <h2 className={`text-xl sm:text-2xl md:text-3xl font-serif font-semibold
text-white tracking-tight"> ${textClasses.primary} tracking-tight`}>
Send Us a Message Send Us a Message
</h2> </h2>
</div> </div>
@@ -328,9 +381,11 @@ const ContactPage: React.FC = () => {
)} )}
{} {}
<div> <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"> <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> Full Name <span className="text-[var(--luxury-gold)] font-semibold">*</span>
</span> </span>
</label> </label>
@@ -340,8 +395,8 @@ const ContactPage: React.FC = () => {
name="name" name="name"
value={formData.name} value={formData.name}
onChange={handleChange} 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 className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
text-white text-sm sm:text-base placeholder-gray-500/60 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 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
${errors.name ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`} ${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 className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-5 md:gap-6 lg:gap-7">
{} {}
<div> <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"> <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> Email <span className="text-[var(--luxury-gold)] font-semibold">*</span>
</span> </span>
</label> </label>
@@ -368,8 +425,8 @@ const ContactPage: React.FC = () => {
name="email" name="email"
value={formData.email} value={formData.email}
onChange={handleChange} 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 className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
text-white text-sm sm:text-base placeholder-gray-500/60 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 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
${errors.email ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`} ${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> <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"> <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" /> {React.createElement(getIconComponent(pageContent?.contact_icons?.phone_field, Phone), {
Phone <span className="text-gray-500 text-xs">(Optional)</span> 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> </span>
</label> </label>
<input <input
@@ -394,10 +453,10 @@ const ContactPage: React.FC = () => {
name="phone" name="phone"
value={formData.phone} value={formData.phone}
onChange={handleChange} 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 className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 border-[var(--luxury-gold)]/30 rounded-lg
text-white text-sm sm:text-base placeholder-gray-500/60 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 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" placeholder="+1 (555) 123-4567"
/> />
</div> </div>
@@ -405,9 +464,11 @@ const ContactPage: React.FC = () => {
{} {}
<div> <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"> <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> Subject <span className="text-[var(--luxury-gold)] font-semibold">*</span>
</span> </span>
</label> </label>
@@ -417,8 +478,8 @@ const ContactPage: React.FC = () => {
name="subject" name="subject"
value={formData.subject} value={formData.subject}
onChange={handleChange} 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 className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
text-white text-sm sm:text-base placeholder-gray-500/60 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 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
${errors.subject ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`} ${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> <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"> <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> Message <span className="text-[var(--luxury-gold)] font-semibold">*</span>
</span> </span>
</label> </label>
@@ -443,8 +506,8 @@ const ContactPage: React.FC = () => {
value={formData.message} value={formData.message}
onChange={handleChange} onChange={handleChange}
rows={6} 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 className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
text-white text-sm sm:text-base placeholder-gray-500/60 resize-none 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 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
${errors.message ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`} ${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> <span className="relative z-10">Send Message</span>
</> </>
)} )}

View File

@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService'; import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService'; import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading'; import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer'; import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
const FAQPage: React.FC = () => { const FAQPage: React.FC = () => {
const { settings } = useCompanySettings(); const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null); const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true); 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(() => { useEffect(() => {
const fetchPageContent = async () => { const fetchPageContent = async () => {
try { try {
@@ -26,22 +33,26 @@ const FAQPage: React.FC = () => {
const tempDiv = document.createElement('div'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent; 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('*'); const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => { allElements.forEach((el) => {
const htmlEl = el as HTMLElement; const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase(); 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)) {
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { htmlEl.style.color = headingColor;
htmlEl.style.color = '#ffffff'; } else if (['strong', 'b'].includes(tagName)) {
} else if (['strong', 'b'].includes(tagName)) { htmlEl.style.color = accentColor;
htmlEl.style.color = '#d4af37'; } else if (tagName === 'a') {
} else if (tagName === 'a') { htmlEl.style.color = accentColor;
htmlEl.style.color = '#d4af37'; } else {
} else { htmlEl.style.color = bodyColor;
htmlEl.style.color = '#d1d5db';
}
} }
}); });
@@ -83,7 +94,7 @@ const FAQPage: React.FC = () => {
if (!pageContent) { if (!pageContent) {
return ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)', marginRight: 'calc(50% - 50vw)',
@@ -95,8 +106,8 @@ const FAQPage: React.FC = () => {
> >
<div className="text-center"> <div className="text-center">
<HelpCircle className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" /> <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> <h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Frequently Asked Questions</h1>
<p className="text-gray-400">This page is currently unavailable.</p> <p className={textClasses.muted}>This page is currently unavailable.</p>
<Link <Link
to="/" to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300" 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 ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: '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"> <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)]" /> <HelpCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div> </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'} {pageContent.title || 'Frequently Asked Questions'}
</h1> </h1>
{pageContent.subtitle && ( {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} {pageContent.subtitle}
</p> </p>
)} )}
</div> </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 <div
className="prose prose-invert prose-lg max-w-none text-gray-300 className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold 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-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 ${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-ul:text-gray-300 prose-ul:font-light prose-ul:my-4 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-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-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline 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 style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
style={{ color: '#d1d5db' }}
dangerouslySetInnerHTML={createSanitizedHtml( 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> </div>
{settings.company_email && ( {settings.company_email && (
<div className="mt-8 text-center"> <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{' '} Still have questions? Contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline"> <a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email} {settings.company_email}

View File

@@ -29,6 +29,24 @@ import type { Room } from '../../rooms/services/roomService';
import type { PageContent } from '../services/pageContentService'; import type { PageContent } from '../services/pageContentService';
import type { Service } from '../../hotel_services/services/serviceService'; 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 HomePage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { formatCurrency } = useFormatCurrency(); 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"> <div className="relative grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6 lg:gap-8">
{pageContent.stats.map((stat, index) => ( {pageContent.stats.map((stat, index) => {
<div key={`stat-${index}-${stat.label || index}`} className="text-center group relative"> const IconComponent = stat?.icon ? getIconComponent(stat.icon) : null;
{stat?.icon && ( return (
<div className="mb-3 md:mb-4 group-hover:scale-110 transition-transform duration-300 flex items-center justify-center"> <div key={`stat-${index}-${stat.label || index}`} className="text-center group relative">
{stat.icon && (LucideIcons as any)[stat.icon] ? ( {stat?.icon && (
React.createElement((LucideIcons as any)[stat.icon], { <div className="mb-3 md:mb-4 group-hover:scale-110 transition-transform duration-300 flex items-center justify-center">
className: 'w-8 h-8 md:w-10 md:h-10 text-[var(--luxury-gold)] drop-shadow-md' {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">{stat.icon}</span> })
)} ) : (
</div> <span className="text-3xl md:text-4xl">✨</span>
)} )}
{stat?.number && ( </div>
<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} {stat?.number && (
</div> <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}
{stat?.label && ( </div>
<div className="text-gray-300 text-xs sm:text-sm md:text-base font-light tracking-wider uppercase"> )}
{stat.label} {stat?.label && (
</div> <div className="text-gray-300 text-xs sm:text-sm md:text-base font-light tracking-wider uppercase">
)} {stat.label}
</div> </div>
))} )}
</div>
);
})}
</div> </div>
</div> </div>
</section> </section>

View File

@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService'; import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService'; import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading'; import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer'; import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
const PrivacyPolicyPage: React.FC = () => { const PrivacyPolicyPage: React.FC = () => {
const { settings } = useCompanySettings(); const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null); const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true); 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(() => { useEffect(() => {
const fetchPageContent = async () => { const fetchPageContent = async () => {
try { try {
@@ -28,24 +35,26 @@ const PrivacyPolicyPage: React.FC = () => {
const tempDiv = document.createElement('div'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent; 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('*'); const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => { allElements.forEach((el) => {
const htmlEl = el as HTMLElement; const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase(); const tagName = htmlEl.tagName.toLowerCase();
const currentColor = htmlEl.style.color;
// Only add color if not already set if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { htmlEl.style.color = headingColor;
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { } else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = '#ffffff'; htmlEl.style.color = accentColor;
} else if (['strong', 'b'].includes(tagName)) { } else if (tagName === 'a') {
htmlEl.style.color = '#d4af37'; htmlEl.style.color = accentColor;
} else if (tagName === 'a') { } else {
htmlEl.style.color = '#d4af37'; htmlEl.style.color = bodyColor;
} else {
htmlEl.style.color = '#d1d5db';
}
} }
}); });
@@ -88,7 +97,7 @@ const PrivacyPolicyPage: React.FC = () => {
if (!pageContent) { if (!pageContent) {
return ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)', marginRight: 'calc(50% - 50vw)',
@@ -100,8 +109,8 @@ const PrivacyPolicyPage: React.FC = () => {
> >
<div className="text-center"> <div className="text-center">
<Shield className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" /> <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> <h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Privacy Policy</h1>
<p className="text-gray-400">This page is currently unavailable.</p> <p className={textClasses.muted}>This page is currently unavailable.</p>
<Link <Link
to="/" to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300" 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 ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: '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"> <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)]" /> <Shield className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div> </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'} {pageContent.title || 'Privacy Policy'}
</h1> </h1>
{pageContent.subtitle && ( {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} {pageContent.subtitle}
</p> </p>
)} )}
</div> </div>
{/* Content */} {/* 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 <div
className="prose prose-invert prose-lg max-w-none text-gray-300 className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold 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-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 ${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-ul:text-gray-300 prose-ul:font-light prose-ul:my-4 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-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-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline 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 style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
style={{ color: '#d1d5db' }}
dangerouslySetInnerHTML={createSanitizedHtml( 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> </div>
@@ -174,7 +185,7 @@ const PrivacyPolicyPage: React.FC = () => {
<div className="mt-8 space-y-4"> <div className="mt-8 space-y-4">
{settings.company_email && ( {settings.company_email && (
<div className="text-center"> <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{' '} For questions about this policy, contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline"> <a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email} {settings.company_email}

View File

@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService'; import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService'; import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading'; import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer'; import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
const RefundsPolicyPage: React.FC = () => { const RefundsPolicyPage: React.FC = () => {
const { settings } = useCompanySettings(); const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null); const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true); 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(() => { useEffect(() => {
const fetchPageContent = async () => { const fetchPageContent = async () => {
try { try {
@@ -28,24 +35,26 @@ const RefundsPolicyPage: React.FC = () => {
const tempDiv = document.createElement('div'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent; 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('*'); const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => { allElements.forEach((el) => {
const htmlEl = el as HTMLElement; const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase(); const tagName = htmlEl.tagName.toLowerCase();
const currentColor = htmlEl.style.color;
// Only add color if not already set if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { htmlEl.style.color = headingColor;
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { } else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = '#ffffff'; htmlEl.style.color = accentColor;
} else if (['strong', 'b'].includes(tagName)) { } else if (tagName === 'a') {
htmlEl.style.color = '#d4af37'; htmlEl.style.color = accentColor;
} else if (tagName === 'a') { } else {
htmlEl.style.color = '#d4af37'; htmlEl.style.color = bodyColor;
} else {
htmlEl.style.color = '#d1d5db';
}
} }
}); });
@@ -88,7 +97,7 @@ const RefundsPolicyPage: React.FC = () => {
if (!pageContent) { if (!pageContent) {
return ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)', marginRight: 'calc(50% - 50vw)',
@@ -100,8 +109,8 @@ const RefundsPolicyPage: React.FC = () => {
> >
<div className="text-center"> <div className="text-center">
<RefreshCw className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" /> <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> <h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Refunds Policy</h1>
<p className="text-gray-400">This page is currently unavailable.</p> <p className={textClasses.muted}>This page is currently unavailable.</p>
<Link <Link
to="/" to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300" 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 ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: '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"> <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)]" /> <RefreshCw className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div> </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'} {pageContent.title || 'Refunds Policy'}
</h1> </h1>
{pageContent.subtitle && ( {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} {pageContent.subtitle}
</p> </p>
)} )}
</div> </div>
{/* Content */} {/* 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 <div
className="prose prose-invert prose-lg max-w-none text-gray-300 className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold 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-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 ${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-ul:text-gray-300 prose-ul:font-light prose-ul:my-4 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-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-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline 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 style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
style={{ color: '#d1d5db' }}
dangerouslySetInnerHTML={createSanitizedHtml( 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> </div>
@@ -173,7 +184,7 @@ const RefundsPolicyPage: React.FC = () => {
{/* Footer Note */} {/* Footer Note */}
{settings.company_email && ( {settings.company_email && (
<div className="mt-8 text-center"> <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{' '} For refund inquiries, contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline"> <a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email} {settings.company_email}

View File

@@ -7,6 +7,8 @@ import serviceService from '../../hotel_services/services/serviceService';
import Loading from '../../../shared/components/Loading'; import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer'; import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency'; import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
interface ServiceSection { interface ServiceSection {
type: 'hero' | 'text' | 'image' | 'gallery' | 'quote' | 'features' | 'cta' | 'video'; type: 'hero' | 'text' | 'image' | 'gallery' | 'quote' | 'features' | 'cta' | 'video';
@@ -47,12 +49,17 @@ interface ServiceDetail {
const ServiceDetailPage: React.FC = () => { const ServiceDetailPage: React.FC = () => {
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { theme } = useTheme();
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();
const [service, setService] = useState<ServiceDetail | null>(null); const [service, setService] = useState<ServiceDetail | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [relatedServices, setRelatedServices] = useState<ServiceDetail[]>([]); const [relatedServices, setRelatedServices] = useState<ServiceDetail[]>([]);
const [pageContent, setPageContent] = useState<PageContent | null>(null); 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(() => { useEffect(() => {
if (slug) { if (slug) {
fetchService(); fetchService();
@@ -288,9 +295,9 @@ const ServiceDetailPage: React.FC = () => {
if (!service) { if (!service) {
return ( 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"> <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"> <Link to="/services" className="text-[var(--luxury-gold)] hover:underline">
Back to Services Back to Services
</Link> </Link>
@@ -300,7 +307,7 @@ const ServiceDetailPage: React.FC = () => {
} }
return ( 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 */} {/* Hero Section with Featured Image - Enhanced Luxury */}
{service.image && ( {service.image && (
<div className="relative w-full h-[60vh] min-h-[500px] max-h-[800px] overflow-hidden"> <div className="relative w-full h-[60vh] min-h-[500px] max-h-[800px] overflow-hidden">
@@ -338,11 +345,11 @@ const ServiceDetailPage: React.FC = () => {
)} )}
</div> </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} {service.title}
</h1> </h1>
{service.description && ( {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} {service.description}
</p> </p>
)} )}
@@ -352,7 +359,7 @@ const ServiceDetailPage: React.FC = () => {
{formatCurrency(service.price)} {formatCurrency(service.price)}
</span> </span>
{service.unit && ( {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} / {service.unit}
</span> </span>
)} )}
@@ -375,7 +382,7 @@ const ServiceDetailPage: React.FC = () => {
{!service.image && ( {!service.image && (
<Link <Link
to="/services" 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" /> <ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
<span className="font-medium">Back to Services</span> <span className="font-medium">Back to Services</span>
@@ -422,7 +429,7 @@ const ServiceDetailPage: React.FC = () => {
{formatCurrency(service.price)} {formatCurrency(service.price)}
</span> </span>
{service.unit && ( {service.unit && (
<span className="text-xl text-gray-400 font-light"> <span className={`text-xl ${textClasses.muted} font-light`}>
/ {service.unit} / {service.unit}
</span> </span>
)} )}
@@ -513,12 +520,12 @@ const ServiceDetailPage: React.FC = () => {
</div> </div>
<div className="relative px-8 py-20 md:px-16 md:py-32 text-center"> <div className="relative px-8 py-20 md:px-16 md:py-32 text-center">
{section.title && ( {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} {section.title}
</h2> </h2>
)} )}
{section.content && ( {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} {section.content}
</p> </p>
)} )}
@@ -529,17 +536,17 @@ const ServiceDetailPage: React.FC = () => {
{/* Text Section */} {/* Text Section */}
{section.type === 'text' && ( {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.alignment === 'center' ? 'text-center' : section.alignment === 'right' ? 'text-right' : 'text-left'
}`}> }`}>
{section.title && ( {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} {section.title}
</h3> </h3>
)} )}
{section.content && ( {section.content && (
<div <div
className="text-gray-300 font-light leading-relaxed text-lg" className={`${textClasses.secondary} font-light leading-relaxed text-lg`}
dangerouslySetInnerHTML={createSanitizedHtml(section.content)} 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"> <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" /> <img src={section.image} alt={section.title || 'Service image'} className="w-full h-auto" />
{section.title && ( {section.title && (
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] px-6 py-4 border-t border-[var(--luxury-gold)]/20"> <div className={`${cardClasses} 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> <p className={`${textClasses.muted} text-sm font-light italic text-center`}>{section.title}</p>
</div> </div>
)} )}
</div> </div>
@@ -666,7 +673,7 @@ const ServiceDetailPage: React.FC = () => {
{/* Related Services */} {/* Related Services */}
{relatedServices.length > 0 && ( {relatedServices.length > 0 && (
<div className="mt-16 pt-12 border-t border-[var(--luxury-gold)]/20"> <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"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{relatedServices.map((relatedService) => ( {relatedServices.map((relatedService) => (
<Link <Link
@@ -684,11 +691,11 @@ const ServiceDetailPage: React.FC = () => {
</div> </div>
)} )}
<div className="p-4"> <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} {relatedService.title}
</h3> </h3>
{relatedService.description && ( {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} {relatedService.description}
</p> </p>
)} )}

View File

@@ -6,8 +6,29 @@ import pageContentService, { PageContent } from '../services/pageContentService'
import serviceService, { Service } from '../../hotel_services/services/serviceService'; import serviceService, { Service } from '../../hotel_services/services/serviceService';
import Loading from '../../../shared/components/Loading'; import Loading from '../../../shared/components/Loading';
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency'; 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 ServicesPage: React.FC = () => {
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null); const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [hotelServices, setHotelServices] = useState<Service[]>([]); const [hotelServices, setHotelServices] = useState<Service[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -156,10 +177,16 @@ const ServicesPage: React.FC = () => {
return <Loading />; 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 ( 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 */} {/* 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 */} {/* Background Effects */}
<div className="absolute inset-0 opacity-10"> <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> <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="flex justify-center mb-2 sm:mb-3 md:mb-4">
<div className="relative group"> <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="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" /> <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> </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"> <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'} {pageContent?.luxury_services_section_title || 'Our Services'}
</span> </span>
</h1> </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> <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'} {pageContent?.luxury_services_section_subtitle || 'Discover our premium services designed to enhance your stay'}
</p> </p>
<div className="mt-4 flex items-center justify-center gap-2 text-[var(--luxury-gold)]/60"> <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..." placeholder="Search services..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} 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>
</div> </div>
@@ -221,13 +250,13 @@ const ServicesPage: React.FC = () => {
{/* Categories Filter - Top Center */} {/* Categories Filter - Top Center */}
{allCategories.length > 0 && ( {allCategories.length > 0 && (
<div className="mb-12 flex justify-center"> <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 <button
onClick={() => setSelectedCategory(null)} onClick={() => setSelectedCategory(null)}
className={`group relative px-6 py-3 rounded-xl text-sm font-medium transition-all duration-300 overflow-hidden ${ className={`group relative px-6 py-3 rounded-xl text-sm font-medium transition-all duration-300 overflow-hidden ${
selectedCategory === null 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-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"> <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 ${ className={`group relative px-6 py-3 rounded-xl text-sm font-medium transition-all duration-300 overflow-hidden ${
selectedCategory === category 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-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"> <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"> <div className="flex flex-col items-center">
{/* Results Count */} {/* Results Count */}
{!loading && filteredServices.length > 0 && ( {!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 Showing {filteredServices.length} of {allServices.length} services
</div> </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"> <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)]" /> <Award className="w-10 h-10 text-[var(--luxury-gold)]" />
</div> </div>
<p className="text-gray-400 text-xl font-light">No services found</p> <p className={`${textClasses.muted} 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-sm mt-2`}>Try adjusting your search or filters</p>
</div> </div>
) : ( ) : (
<> <>
@@ -288,7 +317,7 @@ const ServicesPage: React.FC = () => {
<Link <Link
key={service.id} key={service.id}
to={`/services/${serviceSlug}`} 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` }} style={{ animationDelay: `${index * 50}ms` }}
> >
{/* Premium Glow Effects */} {/* 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 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>
) : ( ) : (
<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] ? ( {service.icon && (LucideIcons as any)[service.icon] ? (
<div className="relative"> <div className="relative">
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div> <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="relative">
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div> <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>
)} )}
</div> </div>
@@ -345,11 +379,11 @@ const ServicesPage: React.FC = () => {
</span> </span>
</div> </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} {service.title}
</h2> </h2>
{service.description && ( {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} {service.description}
</p> </p>
)} )}
@@ -360,7 +394,7 @@ const ServicesPage: React.FC = () => {
{formatCurrency(service.price)} {formatCurrency(service.price)}
</span> </span>
{service.unit && ( {service.unit && (
<span className="text-sm text-gray-400 font-light"> <span className={`text-sm ${textClasses.muted} font-light`}>
/ {service.unit} / {service.unit}
</span> </span>
)} )}

View File

@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService'; import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService'; import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading'; import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer'; import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
const TermsPage: React.FC = () => { const TermsPage: React.FC = () => {
const { settings } = useCompanySettings(); const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null); const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true); 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(() => { useEffect(() => {
const fetchPageContent = async () => { const fetchPageContent = async () => {
try { try {
@@ -28,24 +35,26 @@ const TermsPage: React.FC = () => {
const tempDiv = document.createElement('div'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent; 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('*'); const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => { allElements.forEach((el) => {
const htmlEl = el as HTMLElement; const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase(); const tagName = htmlEl.tagName.toLowerCase();
const currentColor = htmlEl.style.color;
// Only add color if not already set if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') { htmlEl.style.color = headingColor;
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { } else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = '#ffffff'; htmlEl.style.color = accentColor;
} else if (['strong', 'b'].includes(tagName)) { } else if (tagName === 'a') {
htmlEl.style.color = '#d4af37'; htmlEl.style.color = accentColor;
} else if (tagName === 'a') { } else {
htmlEl.style.color = '#d4af37'; htmlEl.style.color = bodyColor;
} else {
htmlEl.style.color = '#d1d5db';
}
} }
}); });
@@ -88,7 +97,7 @@ const TermsPage: React.FC = () => {
if (!pageContent) { if (!pageContent) {
return ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)', marginRight: 'calc(50% - 50vw)',
@@ -100,8 +109,8 @@ const TermsPage: React.FC = () => {
> >
<div className="text-center"> <div className="text-center">
<Scale className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" /> <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> <h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Terms & Conditions</h1>
<p className="text-gray-400">This page is currently unavailable.</p> <p className={textClasses.muted}>This page is currently unavailable.</p>
<Link <Link
to="/" to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300" 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 ( 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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: '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"> <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)]" /> <Scale className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div> </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'} {pageContent.title || 'Terms & Conditions'}
</h1> </h1>
{pageContent.subtitle && ( {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} {pageContent.subtitle}
</p> </p>
)} )}
</div> </div>
{/* Content */} {/* 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 <div
className="prose prose-invert prose-lg max-w-none text-gray-300 className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold 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-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 ${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-ul:text-gray-300 prose-ul:font-light prose-ul:my-4 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-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-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline 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 style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
style={{ color: '#d1d5db' }}
dangerouslySetInnerHTML={createSanitizedHtml( 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> </div>
@@ -173,7 +184,7 @@ const TermsPage: React.FC = () => {
{/* Footer Note */} {/* Footer Note */}
{settings.company_email && ( {settings.company_email && (
<div className="mt-8 text-center"> <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{' '} For questions about these terms, contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline"> <a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email} {settings.company_email}

View File

@@ -21,6 +21,18 @@ export interface PageContent {
email?: string; email?: string;
address?: 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; map_url?: string;
social_links?: { social_links?: {
facebook?: string; facebook?: string;
@@ -46,6 +58,13 @@ export interface PageContent {
features_section_title?: string; features_section_title?: string;
features_section_subtitle?: string; features_section_subtitle?: string;
about_hero_image?: 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; mission?: string;
vision?: string; vision?: string;
team?: Array<{ name: string; role: string; image?: string; bio?: string; social_links?: { linkedin?: string; twitter?: string; email?: 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_text?: string;
services_section_button_link?: string; services_section_button_link?: string;
services_section_limit?: number; services_section_limit?: number;
services_fallback_icon?: string;
luxury_services?: Array<{ luxury_services?: Array<{
icon?: string; icon?: string;
title: string; title: string;
@@ -204,6 +224,18 @@ export interface UpdatePageContentData {
email?: string; email?: string;
address?: 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; map_url?: string;
social_links?: { social_links?: {
facebook?: string; facebook?: string;
@@ -229,6 +261,13 @@ export interface UpdatePageContentData {
features_section_title?: string; features_section_title?: string;
features_section_subtitle?: string; features_section_subtitle?: string;
about_hero_image?: 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; mission?: string;
vision?: string; vision?: string;
team?: Array<{ name: string; role: string; image?: string; bio?: string; social_links?: { linkedin?: string; twitter?: string; email?: 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_text?: string;
services_section_button_link?: string; services_section_button_link?: string;
services_section_limit?: number; services_section_limit?: number;
services_fallback_icon?: string;
luxury_services?: Array<{ luxury_services?: Array<{
icon?: string; icon?: string;
title: string; title: string;

View File

@@ -6,6 +6,7 @@ import useAuthStore from '../../../store/useAuthStore';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import ConfirmationDialog from '../../../shared/components/ConfirmationDialog'; import ConfirmationDialog from '../../../shared/components/ConfirmationDialog';
import { formatWorkingHours } from '../../../shared/utils/format';
interface ChatWidgetProps { interface ChatWidgetProps {
onClose?: () => void; onClose?: () => void;
@@ -405,7 +406,9 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
{!isWithinBusinessHours ? ( {!isWithinBusinessHours ? (
<p className="text-xs text-slate-700/80 font-light flex items-center gap-1"> <p className="text-xs text-slate-700/80 font-light flex items-center gap-1">
<Clock className="w-3 h-3" /> <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> </p>
) : chat?.status === 'pending' ? ( ) : chat?.status === 'pending' ? (
<p className="text-xs text-slate-700/80 font-light">Waiting for staff...</p> <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> <div>
<p className="text-sm font-semibold text-slate-900 mb-1">Chat Hours</p> <p className="text-sm font-semibold text-slate-900 mb-1">Chat Hours</p>
<p className="text-xs text-slate-600 font-light"> <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. Please leave your inquiry below and we'll get back to you as soon as possible.
</p> </p>
</div> </div>
@@ -585,6 +590,18 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
{} {}
{!showVisitorForm && isWithinBusinessHours && ( {!showVisitorForm && isWithinBusinessHours && (
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-slate-50/50 to-white"> <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 ? ( {loading && !chat ? (
<div className="text-center text-slate-500 py-8 font-light"> <div className="text-center text-slate-500 py-8 font-light">
Starting chat... Starting chat...

View File

@@ -30,14 +30,13 @@ interface CurrencyProviderProps {
} }
export const CurrencyProvider: React.FC<CurrencyProviderProps> = ({ children }) => { 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 [isLoading, setIsLoading] = useState<boolean>(true);
const supportedCurrencies = [ const supportedCurrencies = [
CURRENCY.VND,
CURRENCY.USD, CURRENCY.USD,
CURRENCY.EUR, CURRENCY.EUR,
'GBP', CURRENCY.GBP,
'JPY', 'JPY',
'CNY', 'CNY',
'KRW', 'KRW',
@@ -59,14 +58,14 @@ export const CurrencyProvider: React.FC<CurrencyProviderProps> = ({ children })
localStorage.setItem('currency', platformCurrency); localStorage.setItem('currency', platformCurrency);
} else { } else {
setCurrencyState(CURRENCY.VND); setCurrencyState(CURRENCY.USD);
localStorage.setItem('currency', CURRENCY.VND); localStorage.setItem('currency', CURRENCY.USD);
} }
} catch (error) { } catch (error) {
console.error('Error loading platform currency:', error); console.error('Error loading platform currency:', error);
setCurrencyState(CURRENCY.VND); setCurrencyState(CURRENCY.USD);
localStorage.setItem('currency', CURRENCY.VND); localStorage.setItem('currency', CURRENCY.USD);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -81,7 +80,7 @@ export const CurrencyProvider: React.FC<CurrencyProviderProps> = ({ children })
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('currencyChanged', { window.dispatchEvent(new CustomEvent('currencyChanged', {
detail: { currency: localStorage.getItem('currency') || CURRENCY.VND } detail: { currency: localStorage.getItem('currency') || CURRENCY.USD }
})); }));
} }
}; };

View File

@@ -15,6 +15,8 @@ import Recaptcha from '../../../shared/components/Recaptcha';
import { recaptchaService } from '../../system/services/systemSettingsService'; import { recaptchaService } from '../../system/services/systemSettingsService';
import { useAntibotForm } from '../../auth/hooks/useAntibotForm'; import { useAntibotForm } from '../../auth/hooks/useAntibotForm';
import HoneypotField from '../../../shared/components/HoneypotField'; import HoneypotField from '../../../shared/components/HoneypotField';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
interface ReviewSectionProps { interface ReviewSectionProps {
roomId: number; roomId: number;
@@ -41,6 +43,10 @@ type ReviewFormData = {
const ReviewSection: React.FC<ReviewSectionProps> = ({ const ReviewSection: React.FC<ReviewSectionProps> = ({
roomId 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 { isAuthenticated } = useAuthStore();
const { openModal } = useAuthModal(); const { openModal } = useAuthModal();
const [reviews, setReviews] = useState<Review[]>([]); const [reviews, setReviews] = useState<Review[]>([]);
@@ -184,8 +190,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
return ( return (
<div className="space-y-4"> <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"> <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 text-white mb-3 tracking-wide"> <h3 className={`text-sm sm:text-base font-serif font-semibold ${textClasses.primary} mb-3 tracking-wide`}>
Customer Reviews Customer Reviews
</h3> </h3>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -203,7 +209,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
size="sm" size="sm"
/> />
</div> </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 > 0
? `${totalReviews} review${totalReviews !== 1 ? 's' : ''}` ? `${totalReviews} review${totalReviews !== 1 ? 's' : ''}`
: 'No reviews yet'} : 'No reviews yet'}
@@ -214,8 +220,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
{} {}
{isAuthenticated ? ( {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"> <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 text-white mb-3 tracking-wide"> <h4 className={`text-xs sm:text-sm font-serif font-semibold ${textClasses.primary} mb-3 tracking-wide`}>
Write Your Review Write Your Review
</h4> </h4>
<form onSubmit={handleSubmit(onSubmit)} <form onSubmit={handleSubmit(onSubmit)}
@@ -230,8 +236,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
</div> </div>
)} )}
<div> <div>
<label className="block text-[10px] sm:text-xs font-light <label className={`block text-[10px] sm:text-xs font-light
text-gray-300 mb-1.5 tracking-wide" ${textClasses.secondary} mb-1.5 tracking-wide`}
> >
Your Rating Your Rating
</label> </label>
@@ -253,8 +259,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
<div> <div>
<label <label
htmlFor="comment" htmlFor="comment"
className="block text-[10px] sm:text-xs font-light className={`block text-[10px] sm:text-xs font-light
text-gray-300 mb-1.5 tracking-wide" ${textClasses.secondary} mb-1.5 tracking-wide`}
> >
Comment Comment
</label> </label>
@@ -262,11 +268,11 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
{...register('comment')} {...register('comment')}
id="comment" id="comment"
rows={3} rows={3}
className="w-full px-2.5 py-1.5 bg-[#0a0a0a] border className={`w-full px-2.5 py-1.5 ${inputClasses} border
border-[var(--luxury-gold)]/20 rounded-lg text-white placeholder-gray-500 text-xs sm:text-sm 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:ring-2 focus:ring-[var(--luxury-gold)]/50
focus:border-[var(--luxury-gold)] transition-all duration-300 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..." placeholder="Share your experience..."
/> />
{errors.comment && ( {errors.comment && (
@@ -284,7 +290,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
console.error('reCAPTCHA error:', error); console.error('reCAPTCHA error:', error);
setRecaptchaToken(null); setRecaptchaToken(null);
}} }}
theme="dark" theme={theme.theme_layout_mode === 'light' ? 'light' : 'dark'}
size="normal" size="normal"
/> />
</div> </div>
@@ -323,7 +329,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
{} {}
<div> <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}) All Reviews ({reviews.length > 0 ? reviews.length : totalReviews})
</h4> </h4>
@@ -332,25 +338,25 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
{Array.from({ length: 3 }).map((_, index) => ( {Array.from({ length: 3 }).map((_, index) => (
<div <div
key={index} key={index}
className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20 p-3 className={`${cardClasses} rounded-lg border border-[var(--luxury-gold)]/20 p-3
animate-pulse" animate-pulse`}
> >
<div className="h-3 bg-gray-700 <div className={`h-3 ${theme.theme_layout_mode === 'light' ? 'bg-gray-300' : 'bg-gray-700'}
rounded w-1/4 mb-2" rounded w-1/4 mb-2`}
/> />
<div className="h-3 bg-gray-700 <div className={`h-3 ${theme.theme_layout_mode === 'light' ? 'bg-gray-300' : 'bg-gray-700'}
rounded w-3/4" rounded w-3/4`}
/> />
</div> </div>
))} ))}
</div> </div>
) : reviews.length === 0 ? ( ) : 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 No reviews yet
</p> </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! Be the first to review this room!
</p> </p>
</div> </div>
@@ -359,15 +365,15 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
{reviews.map((review) => ( {reviews.map((review) => (
<div <div
key={review.id} key={review.id}
className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20 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" p-3 sm:p-4 backdrop-blur-xl shadow-sm shadow-[var(--luxury-gold)]/5`}
> >
<div className="flex items-start <div className="flex items-start
justify-between mb-2" justify-between mb-2"
> >
<div> <div>
<h5 className="font-semibold <h5 className={`font-semibold
text-white text-xs sm:text-sm" ${textClasses.primary} text-xs sm:text-sm`}
> >
{review.user?.full_name || 'Guest'} {review.user?.full_name || 'Guest'}
</h5> </h5>
@@ -378,15 +384,15 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
rating={review.rating} rating={review.rating}
size="sm" size="sm"
/> />
<span className="text-[10px] sm:text-xs <span className={`text-[10px] sm:text-xs
text-gray-400 font-light" ${textClasses.muted} font-light`}
> >
{formatDate(review.created_at)} {formatDate(review.created_at)}
</span> </span>
</div> </div>
</div> </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} {review.comment}
</p> </p>
</div> </div>

View File

@@ -4,6 +4,8 @@ import 'react-datepicker/dist/react-datepicker.css';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency'; import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
import { Calendar, DollarSign, Users, X } from 'lucide-react'; import { Calendar, DollarSign, Users, X } from 'lucide-react';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
interface RoomFilterProps { interface RoomFilterProps {
onFilterChange?: (filters: FilterValues) => void; onFilterChange?: (filters: FilterValues) => void;
@@ -19,6 +21,10 @@ export interface FilterValues {
} }
const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => { 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 [searchParams, setSearchParams] = useSearchParams();
const { formatCurrency: formatCurrencyUtil } = useFormatCurrency(); const { formatCurrency: formatCurrencyUtil } = useFormatCurrency();
@@ -249,10 +255,10 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
}; };
return ( return (
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] <div className={`${cardClasses}
rounded-xl border border-[var(--luxury-gold)]/30 rounded-xl border border-[var(--luxury-gold)]/30
backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/10 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="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 <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" /> <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> </svg>
</div> </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 Room Filters
</h2> </h2>
</div> </div>
@@ -270,8 +276,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
<div> <div>
<label <label
htmlFor="type" htmlFor="type"
className="block text-sm font-medium className={`block text-sm font-medium
text-gray-200 mb-2 tracking-wide" ${textClasses.secondary} mb-2 tracking-wide`}
> >
Room Type Room Type
</label> </label>
@@ -281,20 +287,20 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
name="type" name="type"
value={filters.type || ''} value={filters.type || ''}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full px-4 py-3.5 bg-[#1a1a1a] border-2 className={`w-full px-4 py-3.5 ${inputClasses} border-2
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base border-[var(--luxury-gold)]/30 rounded-lg text-base
placeholder-gray-400 focus:ring-2 placeholder-gray-400 focus:ring-2
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)] focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
transition-all duration-300 font-normal tracking-wide transition-all duration-300 font-normal tracking-wide
appearance-none cursor-pointer 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="" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>All Room Types</option>
<option value="Standard Room" className="bg-[#1a1a1a] text-white">Standard Room</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="bg-[#1a1a1a] text-white">Deluxe 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="bg-[#1a1a1a] text-white">Luxury 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="bg-[#1a1a1a] text-white">Family 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="bg-[#1a1a1a] text-white">Twin 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> </select>
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none"> <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"> <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> <div>
<label <label
htmlFor="from" 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)]" /> <Calendar className="w-4 h-4 text-[var(--luxury-gold)]" />
Check-in Date Check-in Date
@@ -330,12 +336,12 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
minDate={new Date()} minDate={new Date()}
dateFormat="dd/MM/yyyy" dateFormat="dd/MM/yyyy"
placeholderText="Select check-in" placeholderText="Select check-in"
className="w-full px-4 py-3.5 pl-11 bg-[#1a1a1a] border-2 className={`w-full px-4 py-3.5 pl-11 ${inputClasses} border-2
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base border-[var(--luxury-gold)]/30 rounded-lg text-base
placeholder-gray-400 focus:ring-2 placeholder-gray-400 focus:ring-2
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)] focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
transition-all duration-300 font-normal tracking-wide transition-all duration-300 font-normal tracking-wide
hover:border-[var(--luxury-gold)]/50" hover:border-[var(--luxury-gold)]/50`}
wrapperClassName="w-full" wrapperClassName="w-full"
/> />
{checkInDate && ( {checkInDate && (
@@ -348,8 +354,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
setCheckOutDate(null); setCheckOutDate(null);
} }
}} }}
className="absolute right-3 top-1/2 -translate-y-1/2 className={`absolute right-3 top-1/2 -translate-y-1/2
text-gray-400 hover:text-[var(--luxury-gold)] transition-colors" ${textClasses.muted} hover:text-[var(--luxury-gold)] transition-colors`}
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</button> </button>
@@ -362,7 +368,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
<div> <div>
<label <label
htmlFor="to" 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)]" /> <Calendar className="w-4 h-4 text-[var(--luxury-gold)]" />
Check-out Date Check-out Date
@@ -382,13 +388,13 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
disabled={!checkInDate} disabled={!checkInDate}
dateFormat="dd/MM/yyyy" dateFormat="dd/MM/yyyy"
placeholderText={checkInDate ? "Select check-out" : "Select check-in first"} placeholderText={checkInDate ? "Select check-out" : "Select check-in first"}
className="w-full px-4 py-3.5 pl-11 bg-[#1a1a1a] border-2 className={`w-full px-4 py-3.5 pl-11 ${inputClasses} border-2
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base border-[var(--luxury-gold)]/30 rounded-lg text-base
placeholder-gray-400 focus:ring-2 placeholder-gray-400 focus:ring-2
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)] focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
transition-all duration-300 font-normal tracking-wide transition-all duration-300 font-normal tracking-wide
hover:border-[var(--luxury-gold)]/50 hover:border-[var(--luxury-gold)]/50
disabled:opacity-50 disabled:cursor-not-allowed" disabled:opacity-50 disabled:cursor-not-allowed`}
wrapperClassName="w-full" wrapperClassName="w-full"
/> />
{checkOutDate && ( {checkOutDate && (
@@ -398,8 +404,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
e.stopPropagation(); e.stopPropagation();
setCheckOutDate(null); setCheckOutDate(null);
}} }}
className="absolute right-3 top-1/2 -translate-y-1/2 className={`absolute right-3 top-1/2 -translate-y-1/2
text-gray-400 hover:text-[var(--luxury-gold)] transition-colors" ${textClasses.muted} hover:text-[var(--luxury-gold)] transition-colors`}
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</button> </button>
@@ -408,7 +414,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
w-5 h-5 text-[var(--luxury-gold)] pointer-events-none" /> w-5 h-5 text-[var(--luxury-gold)] pointer-events-none" />
</div> </div>
{checkInDate && !checkOutDate && ( {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) Select check-out date (minimum 1 night stay)
</p> </p>
)} )}
@@ -417,7 +423,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
{} {}
<div> <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)]" /> <DollarSign className="w-4 h-4 text-[var(--luxury-gold)]" />
Price Range Price Range
</label> </label>
@@ -425,8 +431,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
<div> <div>
<label <label
htmlFor="minPrice" htmlFor="minPrice"
className="block text-xs font-normal className={`block text-xs font-normal
text-gray-400 mb-1.5 tracking-wide" ${textClasses.muted} mb-1.5 tracking-wide`}
> >
Minimum Minimum
</label> </label>
@@ -446,20 +452,20 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
placeholder="0" placeholder="0"
inputMode="numeric" inputMode="numeric"
pattern="[0-9.]*" pattern="[0-9.]*"
className="w-full px-4 py-3.5 pl-10 bg-[#1a1a1a] border-2 className={`w-full px-4 py-3.5 pl-10 ${inputClasses} border-2
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base border-[var(--luxury-gold)]/30 rounded-lg text-base
placeholder-gray-400 focus:ring-2 placeholder-gray-400 focus:ring-2
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)] focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
transition-all duration-300 font-normal tracking-wide transition-all duration-300 font-normal tracking-wide
hover:border-[var(--luxury-gold)]/50" hover:border-[var(--luxury-gold)]/50`}
/> />
</div> </div>
</div> </div>
<div> <div>
<label <label
htmlFor="maxPrice" htmlFor="maxPrice"
className="block text-xs font-normal className={`block text-xs font-normal
text-gray-400 mb-1.5 tracking-wide" ${textClasses.muted} mb-1.5 tracking-wide`}
> >
Maximum Maximum
</label> </label>
@@ -479,12 +485,12 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
placeholder="No limit" placeholder="No limit"
inputMode="numeric" inputMode="numeric"
pattern="[0-9.]*" pattern="[0-9.]*"
className="w-full px-4 py-3.5 pl-10 bg-[#1a1a1a] border-2 className={`w-full px-4 py-3.5 pl-10 ${inputClasses} border-2
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base border-[var(--luxury-gold)]/30 rounded-lg text-base
placeholder-gray-400 focus:ring-2 placeholder-gray-400 focus:ring-2
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)] focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
transition-all duration-300 font-normal tracking-wide transition-all duration-300 font-normal tracking-wide
hover:border-[var(--luxury-gold)]/50" hover:border-[var(--luxury-gold)]/50`}
/> />
</div> </div>
</div> </div>
@@ -495,8 +501,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
<div> <div>
<label <label
htmlFor="capacity" htmlFor="capacity"
className="block text-sm font-medium className={`block text-sm font-medium
text-gray-200 mb-2 tracking-wide flex items-center gap-2" ${textClasses.secondary} mb-2 tracking-wide flex items-center gap-2`}
> >
<Users className="w-4 h-4 text-[var(--luxury-gold)]" /> <Users className="w-4 h-4 text-[var(--luxury-gold)]" />
Number of Guests Number of Guests
@@ -513,37 +519,37 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
placeholder="1" placeholder="1"
min="1" min="1"
max="10" max="10"
className="w-full px-4 py-3.5 pl-11 bg-[#1a1a1a] border-2 className={`w-full px-4 py-3.5 pl-11 ${inputClasses} border-2
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base border-[var(--luxury-gold)]/30 rounded-lg text-base
placeholder-gray-400 focus:ring-2 placeholder-gray-400 focus:ring-2
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)] focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
transition-all duration-300 font-normal tracking-wide transition-all duration-300 font-normal tracking-wide
hover:border-[var(--luxury-gold)]/50 hover:border-[var(--luxury-gold)]/50
[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none
[&::-webkit-inner-spin-button]:appearance-none" [&::-webkit-inner-spin-button]:appearance-none`}
/> />
</div> </div>
</div> </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 Amenities
</label> </label>
{availableAmenities.length === 0 ? ( {availableAmenities.length === 0 ? (
<div className="text-sm text-gray-400 font-light bg-[#1a1a1a]/50 <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"> border border-[var(--luxury-gold)]/20 rounded-lg px-4 py-3`}>
Loading amenities... Loading amenities...
</div> </div>
) : ( ) : (
<div className="bg-[#1a1a1a]/50 border border-[var(--luxury-gold)]/20 rounded-lg p-3 <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"> max-h-48 overflow-y-auto custom-scrollbar space-y-2`}>
{availableAmenities.map((amenity) => ( {availableAmenities.map((amenity) => (
<label <label
key={amenity} 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 hover:text-[var(--luxury-gold)] transition-colors cursor-pointer
text-gray-200 group" ${textClasses.secondary} group`}
> >
<input <input
type="checkbox" type="checkbox"
@@ -584,11 +590,11 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
<button <button
type="button" type="button"
onClick={handleReset} 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 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 transition-all font-medium tracking-wide text-sm sm:text-base
touch-manipulation min-h-[44px]" touch-manipulation min-h-[44px]`}
> >
Reset Reset
</button> </button>

View File

@@ -44,6 +44,8 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState(''); 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 allIcons = useMemo(() => {
const icons: string[] = []; const icons: string[] = [];
@@ -59,25 +61,57 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
'memo' 'memo'
]); ]);
for (const iconName in LucideIcons) { try {
// Check if LucideIcons is available
if ( if (!LucideIcons || typeof LucideIcons !== 'object') {
excludedNames.has(iconName) || console.error('IconPicker: LucideIcons is not available');
iconName.startsWith('_') || // Return popular icons as fallback
iconName[0] !== iconName[0].toUpperCase() return popularIcons.filter(icon => icon);
) {
continue;
} }
const iconComponent = (LucideIcons as any)[iconName]; for (const iconName in LucideIcons) {
// Skip if excluded
if (excludedNames.has(iconName)) {
continue;
}
if (typeof iconComponent === 'function') { // Skip if starts with underscore
icons.push(iconName); 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]); }, [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) => { const handleIconSelect = (iconName: string) => {
onChange(iconName); onChange(iconName);
@@ -113,7 +147,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
{selectedIcon ? ( {selectedIcon ? (
<> <>
{React.createElement(selectedIcon, { className: 'w-5 h-5 text-gray-700' })} {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> <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> </div>
<div className="overflow-y-auto p-4"> <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() && ( {!searchQuery.trim() && (
<div className="mb-3"> <div className="mb-3">
@@ -166,7 +205,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
const IconComponent = (LucideIcons as any)[iconName]; const IconComponent = (LucideIcons as any)[iconName];
if (!IconComponent) return null; if (!IconComponent) return null;
const isSelected = value === iconName; const isSelected = normalizedValue === iconName;
const isPopular = !searchQuery.trim() && popularIcons.includes(iconName); const isPopular = !searchQuery.trim() && popularIcons.includes(iconName);
try { try {
@@ -208,8 +247,14 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
</> </>
) : ( ) : (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
<p>No icons found matching "{searchQuery}"</p> {searchQuery.trim() ? (
<p className="text-xs mt-2">Try a different search term</p> <>
<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>
)} )}
</div> </div>

View File

@@ -139,6 +139,10 @@ export interface CompanySettingsResponse {
tax_rate: number; tax_rate: number;
chat_working_hours_start: number; chat_working_hours_start: number;
chat_working_hours_end: 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_at?: string | null;
updated_by?: string | null; updated_by?: string | null;
}; };
@@ -154,6 +158,10 @@ export interface UpdateCompanySettingsRequest {
tax_rate?: number; tax_rate?: number;
chat_working_hours_start?: number; chat_working_hours_start?: number;
chat_working_hours_end?: number; chat_working_hours_end?: number;
bank_name?: string;
bank_account_number?: string;
bank_account_holder?: string;
bank_code?: string;
} }
export interface UploadLogoResponse { export interface UploadLogoResponse {
@@ -219,6 +227,7 @@ export interface ThemeSettingsResponse {
theme_primary_light: string; theme_primary_light: string;
theme_primary_dark: string; theme_primary_dark: string;
theme_primary_accent: string; theme_primary_accent: string;
theme_layout_mode: string; // 'dark' or 'light'
updated_at?: string | null; updated_at?: string | null;
updated_by?: string | null; updated_by?: string | null;
}; };
@@ -230,6 +239,7 @@ export interface UpdateThemeSettingsRequest {
theme_primary_light?: string; theme_primary_light?: string;
theme_primary_dark?: string; theme_primary_dark?: string;
theme_primary_accent?: string; theme_primary_accent?: string;
theme_layout_mode?: string; // 'dark' or 'light'
} }
export interface VerifyRecaptchaRequest { export interface VerifyRecaptchaRequest {

View File

@@ -14,7 +14,6 @@ const CurrencySettingsPage: React.FC = () => {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const currencyNames: Record<string, string> = { const currencyNames: Record<string, string> = {
VND: 'Vietnamese Dong',
USD: 'US Dollar', USD: 'US Dollar',
EUR: 'Euro', EUR: 'Euro',
GBP: 'British Pound', GBP: 'British Pound',

View File

@@ -250,6 +250,7 @@ const PageContentDashboard: React.FC = () => {
services_section_button_text: contents.home.services_section_button_text || '', services_section_button_text: contents.home.services_section_button_text || '',
services_section_button_link: contents.home.services_section_button_link || '', services_section_button_link: contents.home.services_section_button_link || '',
services_section_limit: contents.home.services_section_limit || 6, 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_title: contents.home.luxury_experiences_section_title || '',
luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '', luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '',
luxury_experiences: normalizeArray(contents.home.luxury_experiences), luxury_experiences: normalizeArray(contents.home.luxury_experiences),
@@ -284,19 +285,56 @@ const PageContentDashboard: React.FC = () => {
// Contact // Contact
if (contents.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({ setContactData({
title: contents.contact.title || '', title: contents.contact.title || '',
subtitle: contents.contact.subtitle || '', subtitle: contents.contact.subtitle || '',
description: contents.contact.description || '', description: contents.contact.description || '',
content: contents.contact.content || '', content: contents.contact.content || '',
map_url: contents.contact.map_url || '', 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_title: contents.contact.meta_title || '',
meta_description: contents.contact.meta_description || '', 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 // About
if (contents.about) { if (contents.about) {
const defaultAboutContactIcons = {
location: 'MapPin',
phone: 'Phone',
email: 'Mail',
};
setAboutData({ setAboutData({
title: contents.about.title || '', title: contents.about.title || '',
subtitle: contents.about.subtitle || '', subtitle: contents.about.subtitle || '',
@@ -306,6 +344,12 @@ const PageContentDashboard: React.FC = () => {
values: normalizeArray(contents.about.values), values: normalizeArray(contents.about.values),
features: normalizeArray(contents.about.features), features: normalizeArray(contents.about.features),
about_hero_image: contents.about.about_hero_image || '', 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 || '', mission: contents.about.mission || '',
vision: contents.about.vision || '', vision: contents.about.vision || '',
team: normalizeArray(contents.about.team), team: normalizeArray(contents.about.team),
@@ -314,6 +358,17 @@ const PageContentDashboard: React.FC = () => {
meta_title: contents.about.meta_title || '', meta_title: contents.about.meta_title || '',
meta_description: contents.about.meta_description || '', 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 // Footer
@@ -1354,7 +1409,7 @@ const PageContentDashboard: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<IconPicker <IconPicker
value={amenity?.icon || ''} value={amenity?.icon && amenity.icon.trim() ? amenity.icon : 'Sparkles'}
onChange={(iconName) => { onChange={(iconName) => {
setHomeData((prevData) => { setHomeData((prevData) => {
const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : []; const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
@@ -1550,7 +1605,7 @@ const PageContentDashboard: React.FC = () => {
</div> </div>
<div> <div>
<IconPicker <IconPicker
value={feature?.icon || ''} value={feature?.icon && feature.icon.trim() ? feature.icon : 'Star'}
onChange={(iconName) => { onChange={(iconName) => {
setHomeData((prevData) => { setHomeData((prevData) => {
const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : []; const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
@@ -2031,7 +2086,7 @@ const PageContentDashboard: React.FC = () => {
</div> </div>
<div> <div>
<IconPicker <IconPicker
value={stat?.icon || ''} value={stat?.icon && stat.icon.trim() ? stat.icon : 'TrendingUp'}
onChange={(iconName) => { onChange={(iconName) => {
const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : []; const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
currentStats[index] = { ...currentStats[index], icon: iconName }; currentStats[index] = { ...currentStats[index], icon: iconName };
@@ -2177,7 +2232,7 @@ const PageContentDashboard: React.FC = () => {
</div> </div>
<div> <div>
<IconPicker <IconPicker
value={feature?.icon || ''} value={feature?.icon && feature.icon.trim() ? feature.icon : 'Star'}
onChange={(iconName) => { onChange={(iconName) => {
setHomeData((prevData) => { setHomeData((prevData) => {
const currentFeatures = Array.isArray(prevData.features) ? [...prevData.features] : []; const currentFeatures = Array.isArray(prevData.features) ? [...prevData.features] : [];
@@ -2303,6 +2358,14 @@ const PageContentDashboard: React.FC = () => {
placeholder="6" placeholder="6"
/> />
</div> </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>
</div> </div>
@@ -2371,7 +2434,7 @@ const PageContentDashboard: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<IconPicker <IconPicker
value={experience?.icon || ''} value={experience?.icon && experience.icon.trim() ? experience.icon : 'Sparkles'}
onChange={(iconName) => { onChange={(iconName) => {
const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : []; const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
current[index] = { ...current[index], icon: iconName }; 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 className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<IconPicker <IconPicker
value={award?.icon || ''} value={award?.icon && award.icon.trim() ? award.icon : 'Award'}
onChange={(iconName) => { onChange={(iconName) => {
const current = Array.isArray(homeData.awards) ? [...homeData.awards] : []; const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
current[index] = { ...current[index], icon: iconName }; current[index] = { ...current[index], icon: iconName };
@@ -3607,6 +3670,112 @@ const PageContentDashboard: React.FC = () => {
</div> </div>
</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 className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label> <label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
@@ -3727,6 +3896,15 @@ const PageContentDashboard: React.FC = () => {
)} )}
</div> </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 */} {/* Mission */}
<div> <div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Mission Statement</label> <label className="block text-sm font-semibold text-gray-700 mb-2">Mission Statement</label>
@@ -3792,7 +3970,7 @@ const PageContentDashboard: React.FC = () => {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label> <label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
<IconPicker <IconPicker
value={value.icon || 'Heart'} value={value.icon && value.icon.trim() ? value.icon : 'Heart'}
onChange={(icon: string) => { onChange={(icon: string) => {
setAboutData((prevData) => { setAboutData((prevData) => {
const newValues = [...(prevData.values || [])]; const newValues = [...(prevData.values || [])];
@@ -3883,7 +4061,7 @@ const PageContentDashboard: React.FC = () => {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label> <label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
<IconPicker <IconPicker
value={feature.icon || 'Star'} value={feature.icon && feature.icon.trim() ? feature.icon : 'Star'}
onChange={(icon: string) => { onChange={(icon: string) => {
setAboutData((prevData) => { setAboutData((prevData) => {
const newFeatures = [...(prevData.features || [])]; const newFeatures = [...(prevData.features || [])];
@@ -4262,7 +4440,7 @@ const PageContentDashboard: React.FC = () => {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label> <label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
<IconPicker <IconPicker
value={achievement.icon || 'Award'} value={achievement.icon && achievement.icon.trim() ? achievement.icon : 'Award'}
onChange={(icon: string) => { onChange={(icon: string) => {
setAboutData((prevData) => { setAboutData((prevData) => {
const newAchievements = [...(prevData.achievements || [])]; const newAchievements = [...(prevData.achievements || [])];
@@ -4378,6 +4556,50 @@ const PageContentDashboard: React.FC = () => {
)} )}
</div> </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 className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label> <label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>

View File

@@ -134,6 +134,10 @@ const SettingsPage: React.FC = () => {
tax_rate: 0, tax_rate: 0,
chat_working_hours_start: 9, chat_working_hours_start: 9,
chat_working_hours_end: 17, chat_working_hours_end: 17,
bank_name: '',
bank_account_number: '',
bank_account_holder: '',
bank_code: '',
}); });
const [logoPreview, setLogoPreview] = useState<string | null>(null); const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [faviconPreview, setFaviconPreview] = 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_light: '#f5d76e',
theme_primary_dark: '#c9a227', theme_primary_dark: '#c9a227',
theme_primary_accent: '#e8c547', theme_primary_accent: '#e8c547',
theme_layout_mode: 'dark',
}); });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -165,7 +170,6 @@ const SettingsPage: React.FC = () => {
const [openPaymentModal, setOpenPaymentModal] = useState<'stripe' | 'paypal' | 'borica' | null>(null); const [openPaymentModal, setOpenPaymentModal] = useState<'stripe' | 'paypal' | 'borica' | null>(null);
const currencyNames: Record<string, string> = { const currencyNames: Record<string, string> = {
VND: 'Vietnamese Dong',
USD: 'US Dollar', USD: 'US Dollar',
EUR: 'Euro', EUR: 'Euro',
GBP: 'British Pound', GBP: 'British Pound',
@@ -300,6 +304,10 @@ const SettingsPage: React.FC = () => {
tax_rate: companyRes.data.tax_rate || 0, tax_rate: companyRes.data.tax_rate || 0,
chat_working_hours_start: companyRes.data.chat_working_hours_start || 9, chat_working_hours_start: companyRes.data.chat_working_hours_start || 9,
chat_working_hours_end: companyRes.data.chat_working_hours_end || 17, 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_light: themeRes.data.theme_primary_light || '#f5d76e',
theme_primary_dark: themeRes.data.theme_primary_dark || '#c9a227', theme_primary_dark: themeRes.data.theme_primary_dark || '#c9a227',
theme_primary_accent: themeRes.data.theme_primary_accent || '#e8c547', theme_primary_accent: themeRes.data.theme_primary_accent || '#e8c547',
theme_layout_mode: themeRes.data.theme_layout_mode || 'dark',
}); });
} catch (error: any) { } catch (error: any) {
toast.error( toast.error(
@@ -2722,6 +2731,82 @@ const SettingsPage: React.FC = () => {
</p> </p>
</div> </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"> <div className="space-y-4">
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide"> <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>
</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="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" /> <Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-800"> <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"> <p className="text-xs">
Changes to theme colors will be applied immediately across all frontend pages. 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 colors will replace the default gold/yellow theme throughout the application. Use hex color codes (e.g., #d4af37) for best results. The layout mode controls whether pages use dark (black) or light (white) backgrounds.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -39,12 +39,14 @@ import { getBookingStatusConfig } from '../../shared/utils/bookingUtils';
import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils'; import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils';
import { BOOKING_STATUS, PAYMENT_METHOD, PAYMENT_STATUS, PAYMENT_TYPE } from '../../shared/constants/bookingConstants'; import { BOOKING_STATUS, PAYMENT_METHOD, PAYMENT_STATUS, PAYMENT_TYPE } from '../../shared/constants/bookingConstants';
import useAuthStore from '../../store/useAuthStore'; import useAuthStore from '../../store/useAuthStore';
import { useCompanySettings } from '../../shared/contexts/CompanySettingsContext';
const BookingSuccessPage: React.FC = () => { const BookingSuccessPage: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();
const { userInfo } = useAuthStore(); const { userInfo } = useAuthStore();
const { settings } = useCompanySettings();
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
const [booking, setBooking] = useState<Booking | null>( const [booking, setBooking] = useState<Booking | null>(
@@ -260,7 +262,10 @@ const BookingSuccessPage: React.FC = () => {
const qrCodeUrl = booking const qrCodeUrl = booking
? generateQRCode( ? generateQRCode(
booking.booking_number, booking.booking_number,
booking.total_price booking.total_price,
settings.bank_code || undefined,
settings.bank_account_number || undefined,
settings.bank_account_holder || undefined
) )
: null; : null;
@@ -687,15 +692,15 @@ const BookingSuccessPage: React.FC = () => {
> >
<p> <p>
<strong>Bank:</strong> <strong>Bank:</strong>
Vietcombank (VCB) {settings.bank_name || 'Not configured'}
</p> </p>
<p> <p>
<strong>Account Number:</strong> <strong>Account Number:</strong>
0123456789 {settings.bank_account_number || 'Not configured'}
</p> </p>
<p> <p>
<strong>Account Holder:</strong> <strong>Account Holder:</strong>
KHACH SAN ABC {settings.bank_account_holder || 'Not configured'}
</p> </p>
<p> <p>
<strong>Amount:</strong>{' '} <strong>Amount:</strong>{' '}

View File

@@ -30,6 +30,7 @@ import { validateBookingId } from '../../shared/utils/routeValidation';
import { validateAndHandleBookingOwnership } from '../../shared/utils/ownershipValidation'; import { validateAndHandleBookingOwnership } from '../../shared/utils/ownershipValidation';
import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils'; import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils';
import { PAYMENT_METHOD, PAYMENT_STATUS } from '../../shared/constants/bookingConstants'; import { PAYMENT_METHOD, PAYMENT_STATUS } from '../../shared/constants/bookingConstants';
import { useCompanySettings } from '../../shared/contexts/CompanySettingsContext';
const PaymentConfirmationPage: React.FC = () => { const PaymentConfirmationPage: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
@@ -37,6 +38,7 @@ const PaymentConfirmationPage: React.FC = () => {
const { isAuthenticated, userInfo } = useAuthStore(); const { isAuthenticated, userInfo } = useAuthStore();
const { openModal } = useAuthModal(); const { openModal } = useAuthModal();
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();
const { settings } = useCompanySettings();
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
const [booking, setBooking] = useState<Booking | null>( const [booking, setBooking] = useState<Booking | null>(
@@ -256,7 +258,10 @@ const PaymentConfirmationPage: React.FC = () => {
const qrCodeUrl = generateQRCode( const qrCodeUrl = generateQRCode(
booking.booking_number, booking.booking_number,
booking.total_price booking.total_price,
settings.bank_code || undefined,
settings.bank_account_number || undefined,
settings.bank_account_holder || undefined
); );
return ( return (
@@ -371,15 +376,15 @@ const PaymentConfirmationPage: React.FC = () => {
> >
<p> <p>
<strong>Bank:</strong> <strong>Bank:</strong>
Vietcombank (VCB) {settings.bank_name || 'Not configured'}
</p> </p>
<p> <p>
<strong>Account Number:</strong> <strong>Account Number:</strong>
0123456789 {settings.bank_account_number || 'Not configured'}
</p> </p>
<p> <p>
<strong>Account Holder:</strong> <strong>Account Holder:</strong>
KHACH SAN ABC {settings.bank_account_holder || 'Not configured'}
</p> </p>
<p> <p>
<strong>Amount:</strong>{' '} <strong>Amount:</strong>{' '}
@@ -408,12 +413,18 @@ const PaymentConfirmationPage: React.FC = () => {
> >
Scan QR code to transfer Scan QR code to transfer
</p> </p>
<img {qrCodeUrl ? (
src={qrCodeUrl} <img
alt="QR Code" src={qrCodeUrl}
className="w-48 h-48 border-2 alt="QR Code"
border-gray-200 rounded-lg" 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> </div>
</div> </div>

View File

@@ -17,8 +17,8 @@ const PaymentResultPage: React.FC = () => {
const { settings } = useCompanySettings(); const { settings } = useCompanySettings();
const supportEmail = settings.company_email || 'support@hotel.com'; const supportEmail = settings.company_email || null;
const supportPhone = settings.company_phone || '1900 xxxx'; const supportPhone = settings.company_phone || null;
const status = searchParams.get('status'); const status = searchParams.get('status');
const bookingId = searchParams.get('bookingId'); const bookingId = searchParams.get('bookingId');

View File

@@ -21,8 +21,14 @@ import LuxuryBookingModal from '../../features/bookings/components/LuxuryBooking
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext'; import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { logger } from '../../shared/utils/logger'; 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 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 { room_number } = useParams<{ room_number: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();
@@ -172,7 +178,7 @@ const RoomDetailPage: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<div <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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: '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="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="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-[600px] ${cardClasses} 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-12 ${cardClasses} 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-6 ${cardClasses} rounded-lg w-2/3 border border-[var(--luxury-gold)]/10`} />
</div> </div>
</div> </div>
</div> </div>
@@ -196,7 +202,7 @@ const RoomDetailPage: React.FC = () => {
if (error || !room) { if (error || !room) {
return ( return (
<div <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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)', marginRight: 'calc(50% - 50vw)',
@@ -236,7 +242,7 @@ const RoomDetailPage: React.FC = () => {
return ( return (
<div <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={{ style={{
marginLeft: 'calc(50% - 50vw)', marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)', marginRight: 'calc(50% - 50vw)',
@@ -310,7 +316,7 @@ const RoomDetailPage: React.FC = () => {
: 'Maintenance'} : 'Maintenance'}
</div> </div>
{room.status === 'occupied' && bookedUntilDate && ( {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' })} Booked until {bookedUntilDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</p> </p>
)} )}
@@ -322,10 +328,12 @@ const RoomDetailPage: React.FC = () => {
</div> </div>
</div> </div>
<h1 className="text-xl sm:text-2xl lg:text-3xl font-serif font-semibold <h1 className={`text-xl sm:text-2xl lg:text-3xl font-serif font-semibold
text-white mb-2 tracking-tight leading-tight ${textClasses.primary} mb-2 tracking-tight leading-tight
bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white ${theme.theme_layout_mode === 'light'
bg-clip-text text-transparent" ? '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} {roomType?.name}
</h1> </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="grid grid-cols-1 md:grid-cols-3 gap-2 sm:gap-3 mb-3">
<div className="flex items-center gap-2 <div className={`flex items-center gap-2
p-2 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] p-2 ${cardClasses}
rounded-lg border border-[var(--luxury-gold)]/20 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 <div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
border border-[var(--luxury-gold)]/30"> border border-[var(--luxury-gold)]/30">
<MapPin className="w-3.5 h-3.5 text-[var(--luxury-gold)]" /> <MapPin className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
</div> </div>
<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 Location
</p> </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} Room {room.room_number} - Floor {room.floor}
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2 <div className={`flex items-center gap-2
p-2 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] p-2 ${cardClasses}
rounded-lg border border-[var(--luxury-gold)]/20" rounded-lg border border-[var(--luxury-gold)]/20`}
> >
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg <div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
border border-[var(--luxury-gold)]/30"> border border-[var(--luxury-gold)]/30">
<Users className="w-3.5 h-3.5 text-[var(--luxury-gold)]" /> <Users className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
</div> </div>
<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 Capacity
</p> </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 {room?.capacity || roomType?.capacity || 0} guests
</p> </p>
</div> </div>
</div> </div>
{room.average_rating != null && ( {room.average_rating != null && (
<div className="flex items-center gap-2 <div className={`flex items-center gap-2
p-2 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] p-2 ${cardClasses}
rounded-lg border border-[var(--luxury-gold)]/20 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 <div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
border border-[var(--luxury-gold)]/30"> border border-[var(--luxury-gold)]/30">
<Star className="w-3.5 h-3.5 text-[var(--luxury-gold)] fill-[var(--luxury-gold)]" /> <Star className="w-3.5 h-3.5 text-[var(--luxury-gold)] fill-[var(--luxury-gold)]" />
</div> </div>
<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 Rating
</p> </p>
<div className="flex items-center gap-1"> <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)} {Number(room.average_rating).toFixed(1)}
</p> </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}) ({room.total_reviews || 0})
</span> </span>
</div> </div>
@@ -401,23 +409,23 @@ const RoomDetailPage: React.FC = () => {
{} {}
{(room?.description || roomType?.description) && ( {(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 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="flex items-center gap-2 mb-2">
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg <div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
border border-[var(--luxury-gold)]/30"> border border-[var(--luxury-gold)]/30">
<Award className="w-3.5 h-3.5 text-[var(--luxury-gold)]" /> <Award className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
</div> </div>
<h2 className="text-sm sm:text-base font-serif font-semibold <h2 className={`text-sm sm:text-base font-serif font-semibold
text-white tracking-wide" ${textClasses.primary} tracking-wide`}
> >
{room?.description ? 'Room Description' : 'Room Type Description'} {room?.description ? 'Room Description' : 'Room Type Description'}
</h2> </h2>
</div> </div>
<p className="text-gray-300 leading-relaxed <p className={`${textClasses.secondary} leading-relaxed
font-light tracking-wide text-xs sm:text-sm" font-light tracking-wide text-xs sm:text-sm`}
> >
{room?.description || roomType?.description} {room?.description || roomType?.description}
</p> </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 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="flex items-center gap-2 mb-2">
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg <div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
border border-[var(--luxury-gold)]/30"> border border-[var(--luxury-gold)]/30">
<Sparkles className="w-3.5 h-3.5 text-[var(--luxury-gold)]" /> <Sparkles className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
</div> </div>
<h2 className="text-sm sm:text-base font-serif font-semibold <h2 className={`text-sm sm:text-base font-serif font-semibold
text-white tracking-wide" ${textClasses.primary} tracking-wide`}
> >
Amenities & Features Amenities & Features
</h2> </h2>
@@ -461,14 +469,14 @@ const RoomDetailPage: React.FC = () => {
{} {}
<aside className="lg:col-span-4"> <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 rounded-lg border border-[var(--luxury-gold)]/30
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/20 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"> <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 Starting from
</p> </p>
<div className="flex items-baseline gap-1.5"> <div className="flex items-baseline gap-1.5">
@@ -480,7 +488,7 @@ const RoomDetailPage: React.FC = () => {
> >
{formattedPrice} {formattedPrice}
</div> </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 / night
</div> </div>
</div> </div>
@@ -529,7 +537,7 @@ const RoomDetailPage: React.FC = () => {
rounded-lg border border-[var(--luxury-gold)]/20 mb-3" 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" /> <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. <strong className="text-[var(--luxury-gold)]">20% deposit required</strong> to secure your booking. Pay the remaining balance on arrival at the hotel.
</p> </p>
</div> </div>
@@ -542,27 +550,27 @@ const RoomDetailPage: React.FC = () => {
<div className="flex items-center justify-between <div className="flex items-center justify-between
py-1.5 border-b border-[var(--luxury-gold)]/10" 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> <span className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide`}>Room Type</span>
<strong className="text-xs sm:text-sm text-white font-light">{roomType?.name}</strong> <strong className={`text-xs sm:text-sm ${textClasses.primary} font-light`}>{roomType?.name}</strong>
</div> </div>
<div className="flex items-center justify-between <div className="flex items-center justify-between
py-1.5 border-b border-[var(--luxury-gold)]/10" 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-[10px] sm:text-xs ${textClasses.muted} 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-xs sm:text-sm ${textClasses.primary} font-light`}>{(room?.capacity || roomType?.capacity || 0)} guests</span>
</div> </div>
{room?.room_size && ( {room?.room_size && (
<div className="flex items-center justify-between <div className="flex items-center justify-between
py-1.5 border-b border-[var(--luxury-gold)]/10" 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-[10px] sm:text-xs ${textClasses.muted} 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-xs sm:text-sm ${textClasses.primary} font-light`}>{room.room_size}</span>
</div> </div>
)} )}
{room?.view && ( {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'}`}> <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-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide`}>View</span>
<span className="text-xs sm:text-sm text-white font-light">{room.view}</span> <span className={`text-xs sm:text-sm ${textClasses.primary} font-light`}>{room.view}</span>
</div> </div>
)} )}
</div> </div>
@@ -571,9 +579,9 @@ const RoomDetailPage: React.FC = () => {
</div> </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 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} /> <ReviewSection roomId={room.id} />
</div> </div>

View File

@@ -9,8 +9,15 @@ import Pagination from '../../shared/components/Pagination';
import { ArrowLeft, Hotel, Filter, ChevronDown, ChevronUp, Tag, X, CheckCircle } from 'lucide-react'; import { ArrowLeft, Hotel, Filter, ChevronDown, ChevronUp, Tag, X, CheckCircle } from 'lucide-react';
import { logger } from '../../shared/utils/logger'; import { logger } from '../../shared/utils/logger';
import { toast } from 'react-toastify'; 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 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 [searchParams] = useSearchParams();
const [rooms, setRooms] = useState<Room[]>([]); const [rooms, setRooms] = useState<Room[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -146,13 +153,13 @@ const RoomListPage: React.FC = () => {
}, [searchParams]); }, [searchParams]);
return ( 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 */} {/* Promotion Banner */}
{showPromotionBanner && activePromotion && ( {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 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="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="flex items-center gap-3 flex-1">
<div className="p-2 bg-[var(--luxury-gold)]/20 rounded-lg border border-[var(--luxury-gold)]/40"> <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)]" /> <Tag className="w-5 h-5 text-[var(--luxury-gold)]" />
@@ -165,11 +172,11 @@ const RoomListPage: React.FC = () => {
</span> </span>
</div> </div>
{activePromotion.discount && ( {activePromotion.discount && (
<p className="text-xs text-gray-300"> <p className={`text-xs ${textClasses.secondary}`}>
{activePromotion.discount} - {activePromotion.description || 'Valid on bookings'} {activePromotion.discount} - {activePromotion.description || 'Valid on bookings'}
</p> </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 The promotion code will be automatically applied when you book a room
</p> </p>
</div> </div>
@@ -179,14 +186,14 @@ const RoomListPage: React.FC = () => {
className="p-2 hover:bg-[var(--luxury-gold)]/10 rounded-lg transition-colors" className="p-2 hover:bg-[var(--luxury-gold)]/10 rounded-lg transition-colors"
aria-label="Dismiss promotion" 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> </button>
</div> </div>
</div> </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"> <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 <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)]" /> <Hotel className="w-5 h-5 sm:w-5 sm:h-5 text-[var(--luxury-gold)]" />
</div> </div>
</div> </div>
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold <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 ${textClasses.primary} mb-2 sm:mb-3 tracking-tight leading-tight px-2
bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white ${theme.theme_layout_mode === 'light'
bg-clip-text text-transparent" ? '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 Our Rooms & Suites
</h1> </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, Discover our collection of luxurious accommodations,
each designed to provide an exceptional stay each designed to provide an exceptional stay
</p> </p>
@@ -235,18 +244,18 @@ const RoomListPage: React.FC = () => {
<div className="xl:hidden order-1 mb-4"> <div className="xl:hidden order-1 mb-4">
<button <button
onClick={() => setIsFilterOpen(!isFilterOpen)} 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 border border-[var(--luxury-gold)]/30 rounded-xl p-4
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/10 backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/10
flex items-center justify-between gap-3 flex items-center justify-between gap-3
hover:border-[var(--luxury-gold)]/50 hover:shadow-xl hover:shadow-[var(--luxury-gold)]/20 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="flex items-center gap-3">
<div className="p-2 bg-[var(--luxury-gold)]/10 rounded-lg border border-[var(--luxury-gold)]/30"> <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)]" /> <Filter className="w-5 h-5 text-[var(--luxury-gold)]" />
</div> </div>
<span className="text-white font-medium tracking-wide text-base"> <span className={`${textClasses.primary} font-medium tracking-wide text-base`}>
Filters Filters
</span> </span>
</div> </div>
@@ -320,21 +329,21 @@ const RoomListPage: React.FC = () => {
)} )}
{!loading && !error && rooms.length === 0 && ( {!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 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 <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" 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)]" /> <Hotel className="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 text-[var(--luxury-gold)]" />
</div> </div>
<h3 className="text-lg sm:text-xl md:text-2xl font-serif font-semibold <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" ${textClasses.primary} mb-3 sm:mb-4 tracking-wide px-2`}
> >
No matching rooms found No matching rooms found
</h3> </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 Please try adjusting the filters or search differently
</p> </p>
<button <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"> <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{' '} 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 <span className="text-[var(--luxury-gold)] font-medium">{pagination.total}</span> rooms
</p> </p>

View File

@@ -14,7 +14,7 @@ const CurrencyIcon: React.FC<CurrencyIconProps> = ({
currency currency
}) => { }) => {
const { currency: contextCurrency } = useCurrency(); const { currency: contextCurrency } = useCurrency();
const currencyToUse = currency || contextCurrency || 'VND'; const currencyToUse = currency || contextCurrency || 'USD';
const symbol = getCurrencySymbol(currencyToUse); const symbol = getCurrencySymbol(currencyToUse);
return ( return (

View File

@@ -25,7 +25,6 @@ const CurrencySelector: React.FC<CurrencySelectorProps> = ({
const isAdmin = userInfo?.role === 'admin'; const isAdmin = userInfo?.role === 'admin';
const currencyNames: Record<string, string> = { const currencyNames: Record<string, string> = {
VND: 'Vietnamese Dong',
USD: 'US Dollar', USD: 'US Dollar',
EUR: 'Euro', EUR: 'Euro',
GBP: 'British Pound', GBP: 'British Pound',

View File

@@ -29,11 +29,18 @@ import CookiePreferencesLink from './CookiePreferencesLink';
import ChatWidget from '../../features/notifications/components/ChatWidget'; import ChatWidget from '../../features/notifications/components/ChatWidget';
import pageContentService, { type PageContent } from '../../features/content/services/pageContentService'; import pageContentService, { type PageContent } from '../../features/content/services/pageContentService';
import { useCompanySettings } from '../contexts/CompanySettingsContext'; import { useCompanySettings } from '../contexts/CompanySettingsContext';
import { useTheme } from '../contexts/ThemeContext';
import apiClient from '../services/apiClient'; 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 Footer: React.FC = () => {
const { settings } = useCompanySettings(); const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null); 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 [homePageContent, setHomePageContent] = useState<PageContent | null>(null);
const [enabledPages, setEnabledPages] = useState<Set<string>>(new Set()); const [enabledPages, setEnabledPages] = useState<Set<string>>(new Set());
const [apiError, setApiError] = useState(false); const [apiError, setApiError] = useState(false);
@@ -192,7 +199,9 @@ const Footer: React.FC = () => {
}); });
return ( 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 */} {/* 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> <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>
)} )}
<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'} {settings.company_name || pageContent?.title || 'Luxury Hotel'}
</h2> </h2>
<p className="text-xs sm:text-sm text-[var(--luxury-gold)] font-light tracking-[3px] sm:tracking-[4px] uppercase"> <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> </p>
</div> </div>
</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.'} {pageContent?.description || 'Experience unparalleled luxury and world-class hospitality. Your journey to exceptional comfort begins here.'}
</p> </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" 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" /> <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> </div>
); );
})} })}
@@ -266,10 +275,12 @@ const Footer: React.FC = () => {
href={pageContent.social_links.facebook} href={pageContent.social_links.facebook}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" 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> <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> </a>
)} )}
@@ -278,10 +289,12 @@ const Footer: React.FC = () => {
href={pageContent.social_links.twitter} href={pageContent.social_links.twitter}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group relative w-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" 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> <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> </a>
)} )}
@@ -290,10 +303,12 @@ const Footer: React.FC = () => {
href={pageContent.social_links.instagram} href={pageContent.social_links.instagram}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group relative w-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" 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> <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> </a>
)} )}
@@ -302,10 +317,12 @@ const Footer: React.FC = () => {
href={pageContent.social_links.linkedin} href={pageContent.social_links.linkedin}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group relative w-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" 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> <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> </a>
)} )}
@@ -314,10 +331,12 @@ const Footer: React.FC = () => {
href={pageContent.social_links.youtube} href={pageContent.social_links.youtube}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group relative w-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" 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> <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> </a>
)} )}
@@ -327,7 +346,7 @@ const Footer: React.FC = () => {
{/* Quick Links */} {/* Quick Links */}
{quickLinks.length > 0 && ( {quickLinks.length > 0 && (
<div className="lg:col-span-2"> <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="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> <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> </h3>
@@ -336,7 +355,7 @@ const Footer: React.FC = () => {
<li key={link.url}> <li key={link.url}>
<Link <Link
to={link.url} 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="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> <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 */} {/* Guest Services */}
{supportLinks.length > 0 && ( {supportLinks.length > 0 && (
<div className="lg:col-span-2"> <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="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> <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> </h3>
@@ -359,7 +378,7 @@ const Footer: React.FC = () => {
<li key={link.url}> <li key={link.url}>
<Link <Link
to={link.url} 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="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> <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> <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> </h3>
{homePageContent?.newsletter_section_subtitle && ( {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} {homePageContent.newsletter_section_subtitle}
</p> </p>
)} )}
@@ -416,7 +435,9 @@ const Footer: React.FC = () => {
value={newsletterEmail} value={newsletterEmail}
onChange={(e) => setNewsletterEmail(e.target.value)} onChange={(e) => setNewsletterEmail(e.target.value)}
placeholder={homePageContent?.newsletter_placeholder || 'Enter your email'} 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 required
disabled={newsletterSubmitting} disabled={newsletterSubmitting}
/> />
@@ -441,7 +462,7 @@ const Footer: React.FC = () => {
{/* Contact Information */} {/* Contact Information */}
<div className="lg:col-span-2"> <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="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> <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> </h3>
@@ -455,7 +476,7 @@ const Footer: React.FC = () => {
</div> </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 className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
</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 {displayAddress
.split('\n').map((line, i) => ( .split('\n').map((line, i) => (
<React.Fragment key={i}> <React.Fragment key={i}>
@@ -474,7 +495,7 @@ const Footer: React.FC = () => {
</div> </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 className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
</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} {displayPhone}
</a> </a>
</li> </li>
@@ -487,11 +508,29 @@ const Footer: React.FC = () => {
</div> </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 className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
</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} {displayEmail}
</a> </a>
</li> </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> </ul>
)} )}

View File

@@ -18,10 +18,12 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { useClickOutside } from '../hooks/useClickOutside'; import { useClickOutside } from '../hooks/useClickOutside';
import { useCompanySettings } from '../contexts/CompanySettingsContext'; import { useCompanySettings } from '../contexts/CompanySettingsContext';
import { useTheme } from '../contexts/ThemeContext';
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext'; import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
import { normalizeImageUrl } from '../utils/imageUtils'; import { normalizeImageUrl } from '../utils/imageUtils';
import InAppNotificationBell from '../../features/notifications/components/InAppNotificationBell'; import InAppNotificationBell from '../../features/notifications/components/InAppNotificationBell';
import Navbar from './Navbar'; import Navbar from './Navbar';
import { getThemeTextClasses } from '../utils/themeUtils';
interface HeaderProps { interface HeaderProps {
isAuthenticated?: boolean; isAuthenticated?: boolean;
@@ -40,12 +42,14 @@ const Header: React.FC<HeaderProps> = ({
onLogout onLogout
}) => { }) => {
const { settings } = useCompanySettings(); const { settings } = useCompanySettings();
const { theme } = useTheme();
const { openModal } = useAuthModal(); const { openModal } = useAuthModal();
const location = useLocation(); const location = useLocation();
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const displayPhone = settings.company_phone || '+1 (234) 567-890'; const displayPhone = settings.company_phone || null;
const displayEmail = settings.company_email || 'info@luxuryhotel.com'; const displayEmail = settings.company_email || null;
const logoUrl = settings.company_logo_url const logoUrl = settings.company_logo_url
? (settings.company_logo_url.startsWith('http') ? (settings.company_logo_url.startsWith('http')
? settings.company_logo_url ? settings.company_logo_url
@@ -110,12 +114,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
rounded-md transition-all duration-300 rounded-md transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<LogIn className="w-4 h-4" /> <LogIn className="w-4 h-4" />
@@ -160,7 +164,7 @@ const Header: React.FC<HeaderProps> = ({
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
rounded-md transition-all duration-300 rounded-md transition-all duration-300
border-l-2 border-transparent border-l-2 border-transparent
@@ -181,12 +185,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
@@ -201,12 +205,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<Heart className="w-4 h-4" /> <Heart className="w-4 h-4" />
@@ -221,12 +225,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
@@ -241,12 +245,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<Star className="w-4 h-4" /> <Star className="w-4 h-4" />
@@ -261,12 +265,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<Users className="w-4 h-4" /> <Users className="w-4 h-4" />
@@ -281,12 +285,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<AlertCircle className="w-4 h-4" /> <AlertCircle className="w-4 h-4" />
@@ -301,12 +305,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<Bell className="w-4 h-4" /> <Bell className="w-4 h-4" />
@@ -321,12 +325,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<Shield className="w-4 h-4" /> <Shield className="w-4 h-4" />
@@ -344,12 +348,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<User className="w-4 h-4" /> <User className="w-4 h-4" />
@@ -366,12 +370,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<User className="w-4 h-4" /> <User className="w-4 h-4" />
@@ -388,12 +392,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<User className="w-4 h-4" /> <User className="w-4 h-4" />
@@ -410,12 +414,12 @@ const Header: React.FC<HeaderProps> = ({
onTouchStart={(e) => { onTouchStart={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center className={`flex items-center
space-x-2 px-4 py-3 text-white/90 space-x-2 px-4 py-3 ${textClasses.primary}/90
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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' }} style={{ touchAction: 'manipulation' }}
> >
<User className="w-4 h-4" /> <User className="w-4 h-4" />
@@ -446,9 +450,15 @@ const Header: React.FC<HeaderProps> = ({
return ( return (
<header <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="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"> <div className="flex items-center justify-end space-x-6 text-sm">
{displayPhone && ( {displayPhone && (
@@ -492,7 +502,9 @@ const Header: React.FC<HeaderProps> = ({
</div> </div>
)} )}
<div className="flex flex-col min-w-0"> <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} {settings.company_name}
</span> </span>
<span className="text-[8px] sm:text-[9px] md:text-[10px] text-[var(--luxury-gold)] tracking-[0.25em] uppercase font-light hidden sm:block"> <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 <button
onClick={() => openModal('login')} onClick={() => openModal('login')}
className="flex items-center space-x-2 className={`flex items-center space-x-2
px-6 py-2.5 text-white/95 px-6 py-2.5 ${textClasses.primary}/95
hover:text-[var(--luxury-gold)] transition-all duration-300 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 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> <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> </span>
</div> </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} {userInfo?.name}
</span> </span>
</button> </button>
{isUserMenuOpen && ( {isUserMenuOpen && (
<div className="absolute right-0 mt-3 <div className={`absolute right-0 mt-3
w-56 bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-56 ${theme.theme_layout_mode === 'light'
rounded-lg shadow-[0_8px_32px_rgba(0,0,0,0.6)] py-3 border border-[rgba(var(--luxury-gold-rgb),0.2)] ? 'bg-gradient-to-b from-white via-gray-50 to-white border-gray-200'
z-[9999] backdrop-blur-xl animate-fade-in" : '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 <Link
to="/profile" to="/profile"
onClick={() => setIsUserMenuOpen(false)} onClick={() => setIsUserMenuOpen(false)}
className="flex items-center space-x-3 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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"
@@ -620,7 +636,7 @@ const Header: React.FC<HeaderProps> = ({
to="/dashboard" to="/dashboard"
onClick={() => setIsUserMenuOpen(false)} onClick={() => setIsUserMenuOpen(false)}
className="flex items-center space-x-3 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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"
@@ -633,7 +649,7 @@ const Header: React.FC<HeaderProps> = ({
to="/favorites" to="/favorites"
onClick={() => setIsUserMenuOpen(false)} onClick={() => setIsUserMenuOpen(false)}
className="flex items-center space-x-3 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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"
@@ -646,7 +662,7 @@ const Header: React.FC<HeaderProps> = ({
to="/bookings" to="/bookings"
onClick={() => setIsUserMenuOpen(false)} onClick={() => setIsUserMenuOpen(false)}
className="flex items-center space-x-3 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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"
@@ -659,7 +675,7 @@ const Header: React.FC<HeaderProps> = ({
to="/loyalty" to="/loyalty"
onClick={() => setIsUserMenuOpen(false)} onClick={() => setIsUserMenuOpen(false)}
className="flex items-center space-x-3 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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"
@@ -672,7 +688,7 @@ const Header: React.FC<HeaderProps> = ({
to="/group-bookings" to="/group-bookings"
onClick={() => setIsUserMenuOpen(false)} onClick={() => setIsUserMenuOpen(false)}
className="flex items-center space-x-3 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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"
@@ -685,7 +701,7 @@ const Header: React.FC<HeaderProps> = ({
to="/complaints" to="/complaints"
onClick={() => setIsUserMenuOpen(false)} onClick={() => setIsUserMenuOpen(false)}
className="flex items-center space-x-3 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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"
@@ -698,7 +714,7 @@ const Header: React.FC<HeaderProps> = ({
to="/guest-requests" to="/guest-requests"
onClick={() => setIsUserMenuOpen(false)} onClick={() => setIsUserMenuOpen(false)}
className="flex items-center space-x-3 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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"
@@ -711,7 +727,7 @@ const Header: React.FC<HeaderProps> = ({
to="/gdpr" to="/gdpr"
onClick={() => setIsUserMenuOpen(false)} onClick={() => setIsUserMenuOpen(false)}
className="flex items-center space-x-3 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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"
@@ -728,11 +744,11 @@ const Header: React.FC<HeaderProps> = ({
onClick={() => onClick={() =>
setIsUserMenuOpen(false) setIsUserMenuOpen(false)
} }
className="flex items-center className={`flex items-center
space-x-3 px-5 py-3 text-white/95 space-x-3 px-5 py-3 ${textClasses.primary}/95
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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> <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" /> <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={() => onClick={() =>
setIsUserMenuOpen(false) setIsUserMenuOpen(false)
} }
className="flex items-center className={`flex items-center
space-x-3 px-5 py-3 text-white/95 space-x-3 px-5 py-3 ${textClasses.primary}/95
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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> <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" /> <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={() => onClick={() =>
setIsUserMenuOpen(false) setIsUserMenuOpen(false)
} }
className="flex items-center className={`flex items-center
space-x-3 px-5 py-3 text-white/95 space-x-3 px-5 py-3 ${textClasses.primary}/95
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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> <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" /> <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={() => onClick={() =>
setIsUserMenuOpen(false) setIsUserMenuOpen(false)
} }
className="flex items-center className={`flex items-center
space-x-3 px-5 py-3 text-white/95 space-x-3 px-5 py-3 ${textClasses.primary}/95
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)] hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
transition-all duration-300 border-l-2 border-transparent 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> <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" /> <User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />

View File

@@ -2,6 +2,8 @@ import React, { useRef } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Menu, X } from 'lucide-react'; import { Menu, X } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { getThemeTextClasses } from '../utils/themeUtils';
interface NavbarProps { interface NavbarProps {
isMobileMenuOpen: boolean; isMobileMenuOpen: boolean;
@@ -27,6 +29,8 @@ const Navbar: React.FC<NavbarProps> = ({
renderMobileLinksOnly = false, renderMobileLinksOnly = false,
mobileMenuContent mobileMenuContent
}) => { }) => {
const { theme } = useTheme();
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const mobileMenuContainerRef = useRef<HTMLDivElement>(null); const mobileMenuContainerRef = useRef<HTMLDivElement>(null);
const mobileMenuDropdownRef = useRef<HTMLDivElement>(null); const mobileMenuDropdownRef = useRef<HTMLDivElement>(null);
@@ -88,11 +92,11 @@ const Navbar: React.FC<NavbarProps> = ({
key={link.to} key={link.to}
to={link.to} to={link.to}
onClick={handleLinkClick} 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)] hover:bg-[var(--luxury-gold)]/10 hover:text-[var(--luxury-gold)]
rounded-sm transition-all duration-300 rounded-sm transition-all duration-300
border-l-2 border-transparent 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.label}
</Link> </Link>
@@ -109,9 +113,9 @@ const Navbar: React.FC<NavbarProps> = ({
<Link <Link
key={link.to} key={link.to}
to={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 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"> <span className="relative z-10 transition-all duration-300 group-hover:tracking-widest">
{link.label} {link.label}
@@ -148,7 +152,7 @@ const Navbar: React.FC<NavbarProps> = ({
<> <>
{/* Mobile Menu Backdrop */} {/* Mobile Menu Backdrop */}
<div <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={{ style={{
top: '73px', top: '73px',
zIndex: 99998, zIndex: 99998,
@@ -174,12 +178,14 @@ const Navbar: React.FC<NavbarProps> = ({
{/* Mobile Menu Dropdown - Right side on mobile */} {/* Mobile Menu Dropdown - Right side on mobile */}
<div <div
ref={mobileMenuDropdownRef} ref={mobileMenuDropdownRef}
className="md:hidden fixed top-[73px] right-0 w-80 max-w-[85vw] className={`md:hidden fixed top-[73px] right-0 w-80 max-w-[85vw]
bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] ${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)] 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)] py-4 border-l-2 border-[rgba(var(--luxury-gold-rgb),0.4)]
backdrop-blur-xl animate-fade-in backdrop-blur-xl animate-fade-in
overflow-y-auto h-[calc(100vh-73px)]" overflow-y-auto h-[calc(100vh-73px)]`}
style={{ style={{
zIndex: 99999, zIndex: 99999,
position: 'fixed', position: 'fixed',
@@ -227,12 +233,12 @@ const Navbar: React.FC<NavbarProps> = ({
// Ensure mouse events work // Ensure mouse events work
e.stopPropagation(); 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)] hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
rounded-md transition-all duration-300 rounded-md transition-all duration-300
border-l-2 border-transparent border-l-2 border-transparent
hover:border-[var(--luxury-gold)] font-light tracking-wider text-sm uppercase 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={{ style={{
touchAction: 'manipulation', touchAction: 'manipulation',
WebkitTapHighlightColor: 'transparent', WebkitTapHighlightColor: 'transparent',

View File

@@ -17,6 +17,10 @@ type CompanySettings = {
company_address: string; company_address: string;
chat_working_hours_start: number; chat_working_hours_start: number;
chat_working_hours_end: number; chat_working_hours_end: number;
bank_name?: string;
bank_account_number?: string;
bank_account_holder?: string;
bank_code?: string;
}; };
type CompanySettingsContextValue = { type CompanySettingsContextValue = {
@@ -35,6 +39,10 @@ const defaultSettings: CompanySettings = {
company_address: '', company_address: '',
chat_working_hours_start: 9, chat_working_hours_start: 9,
chat_working_hours_end: 17, chat_working_hours_end: 17,
bank_name: '',
bank_account_number: '',
bank_account_holder: '',
bank_code: '',
}; };
const CompanySettingsContext = createContext<CompanySettingsContextValue | undefined>(undefined); 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, 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_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, 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,
}); });

View File

@@ -7,25 +7,28 @@ import React, {
} from 'react'; } from 'react';
import { themeService } from '../../features/system/services/systemSettingsService'; import { themeService } from '../../features/system/services/systemSettingsService';
type ThemeColors = { type ThemeLayoutMode = 'dark' | 'light';
primary: string;
primaryLight: string; type ThemeSettings = {
primaryDark: string; theme_primary_color: string;
primaryAccent: string; theme_primary_light: string;
theme_primary_dark: string;
theme_primary_accent: string;
theme_layout_mode: ThemeLayoutMode;
}; };
type ThemeContextValue = { type ThemeContextValue = {
colors: ThemeColors; theme: ThemeSettings;
isLoading: boolean; isLoading: boolean;
refreshTheme: () => Promise<void>; refreshTheme: () => Promise<void>;
updateColors: (colors: Partial<ThemeColors>) => void;
}; };
const defaultColors: ThemeColors = { const defaultTheme: ThemeSettings = {
primary: '#d4af37', theme_primary_color: '#d4af37',
primaryLight: '#f5d76e', theme_primary_light: '#f5d76e',
primaryDark: '#c9a227', theme_primary_dark: '#c9a227',
primaryAccent: '#e8c547', theme_primary_accent: '#e8c547',
theme_layout_mode: 'dark',
}; };
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined); const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
@@ -42,58 +45,8 @@ interface ThemeProviderProps {
children: ReactNode; 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 }) => { 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 [isLoading, setIsLoading] = useState<boolean>(true);
const loadTheme = async () => { const loadTheme = async () => {
@@ -101,20 +54,34 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
setIsLoading(true); setIsLoading(true);
const response = await themeService.getThemeSettings(); const response = await themeService.getThemeSettings();
if (response.data) { if (response.data) {
const newColors: ThemeColors = { const themeData: ThemeSettings = {
primary: response.data.theme_primary_color || defaultColors.primary, theme_primary_color: response.data.theme_primary_color || defaultTheme.theme_primary_color,
primaryLight: response.data.theme_primary_light || defaultColors.primaryLight, theme_primary_light: response.data.theme_primary_light || defaultTheme.theme_primary_light,
primaryDark: response.data.theme_primary_dark || defaultColors.primaryDark, theme_primary_dark: response.data.theme_primary_dark || defaultTheme.theme_primary_dark,
primaryAccent: response.data.theme_primary_accent || defaultColors.primaryAccent, 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); setTheme(themeData);
applyThemeColors(newColors);
// 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) { } catch (error) {
console.error('Error loading theme settings:', error); console.error('Error loading theme settings:', error);
// Apply default colors on error // Apply defaults
applyThemeColors(defaultColors); document.body.classList.remove('theme-dark', 'theme-light');
setColors(defaultColors); document.body.classList.add('theme-dark');
document.documentElement.classList.remove('theme-dark', 'theme-light');
document.documentElement.classList.add('theme-dark');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -140,23 +107,15 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
await loadTheme(); await loadTheme();
}; };
const updateColors = (newColors: Partial<ThemeColors>) => {
const updatedColors = { ...colors, ...newColors };
setColors(updatedColors);
applyThemeColors(updatedColors);
};
return ( return (
<ThemeContext.Provider <ThemeContext.Provider
value={{ value={{
colors, theme,
isLoading, isLoading,
refreshTheme, refreshTheme,
updateColors,
}} }}
> >
{children} {children}
</ThemeContext.Provider> </ThemeContext.Provider>
); );
}; };

View File

@@ -12,9 +12,9 @@ export const DATE_FORMATS = {
} as const; } as const;
export const CURRENCY = { export const CURRENCY = {
VND: 'VND',
USD: 'USD', USD: 'USD',
EUR: 'EUR', EUR: 'EUR',
GBP: 'GBP',
} as const; } as const;
export const STORAGE_KEYS = { export const STORAGE_KEYS = {

View File

@@ -43,9 +43,7 @@ export const formatCurrency = (
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount; const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
if (isNaN(numAmount)) { if (isNaN(numAmount)) {
return `0 ${currency || 'USD'}`;
if (currency === 'VND') return '0 ₫';
return `0 ${currency || 'VND'}`;
} }
@@ -67,16 +65,7 @@ export const formatCurrency = (
} }
} }
currencyToUse = currencyToUse || 'VND'; currencyToUse = currencyToUse || 'USD';
if (currencyToUse === 'VND') {
return new Intl.NumberFormat('vi-VN', {
style: 'currency',
currency: 'VND',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(numAmount);
}
const localeMap: Record<string, string> = { const localeMap: Record<string, string> = {
@@ -242,3 +231,20 @@ export const truncateText = (
return text.slice(0, maxLength - suffix.length) + suffix; 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)}`;
};

View 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';
};

149
ICON_CONTROL_AUDIT.md Normal file
View File

@@ -0,0 +1,149 @@
# Icon Control Audit Report
## Summary
This document provides a comprehensive audit of icon usage across frontend pages and identifies which icons are controlled from the admin dashboard vs hardcoded.
---
## ✅ Pages with Full Admin Control
### 1. **HomePage** (`Frontend/src/features/content/pages/HomePage.tsx`)
**Status: ✅ Fully Controlled**
All icons are controlled from admin dashboard:
- **Features Section** - Icons controlled via `pageContent.features[].icon`
- **Luxury Features Section** - Icons controlled via `pageContent.luxury_features[].icon`
- **Stats/Achievements Section** - Icons controlled via `pageContent.stats[].icon` (FIXED: now handles lowercase icon names)
- **Amenities Section** - Icons controlled via `pageContent.amenities[].icon`
- **Awards Section** - Icons controlled via `pageContent.awards[].icon`
- **Experiences Section** - Icons controlled via `pageContent.luxury_experiences[].icon`
**Admin Location:** `PageContentDashboard.tsx` → Home Tab
- Features: Line 2179
- Luxury Features: Line 1552
- Stats: Line 2033
- Amenities: Line 1356
- Awards: Line 2524
- Experiences: Line 2373
---
### 2. **ServiceDetailPage** (`Frontend/src/features/content/pages/ServiceDetailPage.tsx`)
**Status: ✅ Fully Controlled**
- Service icon comes from `service.icon` field in database
- Managed via Service Management admin page
---
## ⚠️ Pages with Partial Admin Control
### 3. **AboutPage** (`Frontend/src/features/content/pages/AboutPage.tsx`)
**Status: ⚠️ Partially Controlled**
**Controlled Icons:**
- ✅ Values Section - Icons controlled via `pageContent.values[].icon` (Admin: Line 3794)
- ✅ Features Section - Icons controlled via `pageContent.features[].icon` (Admin: Line 3885)
- ✅ Achievements Section - Icons controlled via `pageContent.achievements[].icon` (Admin: Line 4264)
**Hardcoded Icons:**
- ❌ Hero Section - `Hotel` icon (Line 197) - Hardcoded, no admin control
- ❌ Contact Info Section - `MapPin`, `Phone`, `Mail` icons (Lines 617, 636, 651) - Hardcoded
- ❌ "Learn More" Button - `Hotel` icon (Line 671) - Hardcoded
**Recommendation:** Add admin controls for:
1. Hero section icon (when no hero image is set)
2. Contact info section icons (optional, as these are semantic)
---
### 4. **ServicesPage** (`Frontend/src/features/content/pages/ServicesPage.tsx`)
**Status: ⚠️ Partially Controlled**
**Controlled Icons:**
- ✅ Service icons come from `service.icon` field in database
**Hardcoded Icons:**
- ❌ Fallback icon - `Award` icon (Line 334) - Used when service has no icon
**Recommendation:** This is acceptable as a fallback, but could be made configurable.
---
## ❌ Pages with No Admin Control
### 5. **ContactPage** (`Frontend/src/features/content/pages/ContactPage.tsx`)
**Status: ❌ All Icons Hardcoded**
**Hardcoded Icons:**
- ❌ Hero Section - `Mail` icon (Line 191)
- ❌ Email Contact Info - `Mail` icon (Line 234)
- ❌ Phone Contact Info - `Phone` icon (Line 246)
- ❌ Location Contact Info - `MapPin` icon (Line 258)
- ❌ Form Field Icons:
- `User` icon (Line 333) - Name field
- `Mail` icon (Line 361) - Email field
- `Phone` icon (Line 387) - Phone field
- `MessageSquare` icon (Lines 410, 436) - Subject and Message fields
- ❌ Submit Button - `Send` icon (Line 497)
**Recommendation:** Add admin controls for:
1. Hero section icon
2. Contact info section icons (Email, Phone, Location)
3. Form field icons (optional, as these are semantic UI elements)
**Admin Location:** `PageContentDashboard.tsx` → Contact Tab (currently no icon controls)
---
## 📊 Summary Statistics
| Page | Total Icon Usages | Admin Controlled | Hardcoded | Control % |
|------|------------------|------------------|-----------|-----------|
| HomePage | ~20+ | 20+ | 0 | 100% ✅ |
| ServiceDetailPage | 1 | 1 | 0 | 100% ✅ |
| AboutPage | 8 | 3 | 5 | 37.5% ⚠️ |
| ServicesPage | ~6 | 5 | 1 | 83% ⚠️ |
| ContactPage | 8 | 0 | 8 | 0% ❌ |
---
## 🔧 Recommendations
### High Priority
1. **ContactPage** - Add icon controls for:
- Hero section icon
- Contact info section icons (Email, Phone, Location)
### Medium Priority
2. **AboutPage** - Add icon control for:
- Hero section icon (when no hero image is set)
### Low Priority
3. **ServicesPage** - Consider making fallback icon configurable
4. **ContactPage** - Consider making form field icons configurable (though semantic icons are fine)
---
## 🛠️ Implementation Notes
### Icon Name Handling
- Icons in database may be stored as lowercase (e.g., 'users', 'calendar')
- Lucide React icons use PascalCase (e.g., 'Users', 'Calendar')
- The `getIconComponent` helper function in HomePage.tsx handles this conversion
- Consider adding this helper to other pages that need it
### Admin Icon Picker
- IconPicker component is available at: `Frontend/src/features/system/components/IconPicker.tsx`
- Already integrated in PageContentDashboard for Home and About pages
- Can be easily added to Contact page admin section
---
## 📝 Notes
- Most content icons are properly controlled from admin
- Hardcoded icons are mostly semantic UI elements (form fields, buttons)
- The main gap is the ContactPage which has no icon controls
- AboutPage hero section could benefit from icon control when no image is set

221
SETTINGS_USAGE_AUDIT.md Normal file
View File

@@ -0,0 +1,221 @@
# Settings Usage Audit Report
## Summary
This document provides a comprehensive audit of how frontend pages use information from Settings (email, phone, address, currency, etc.) vs hardcoded values.
---
## ✅ Pages Using Settings Correctly
### 1. **Header Component** (`Frontend/src/shared/components/Header.tsx`)
**Status: ⚠️ Uses Settings with Hardcoded Fallbacks**
- ✅ Uses `useCompanySettings()` hook
- ✅ Uses `settings.company_phone` and `settings.company_email`
-**Hardcoded fallbacks:**
- Phone: `'+1 (234) 567-890'`
- Email: `'info@luxuryhotel.com'`
- ✅ Uses `settings.company_logo_url`
**Recommendation:** Remove hardcoded fallbacks or use empty string/null instead.
---
### 2. **Footer Component** (`Frontend/src/shared/components/Footer.tsx`)
**Status: ✅ Fully Uses Settings**
- ✅ Uses `useCompanySettings()` hook
- ✅ Uses `settings.company_phone`, `settings.company_email`, `settings.company_address`
- ✅ No hardcoded fallbacks (uses `null` if not available)
- ✅ Uses `settings.company_logo_url`
---
### 3. **ContactPage** (`Frontend/src/features/content/pages/ContactPage.tsx`)
**Status: ⚠️ Uses Settings with Hardcoded Fallback Text**
- ✅ Uses `useCompanySettings()` hook
- ✅ Uses `settings.company_phone`, `settings.company_email`, `settings.company_address`
-**Hardcoded fallback text:**
- Phone: `'Available 24/7 for your convenience'` (should be actual phone or null)
- Email: `"We'll respond within 24 hours"` (should be actual email or null)
- Address: `'Visit us at our hotel reception'` (should be actual address or null)
**Recommendation:** Use actual values from settings or show nothing if not available.
---
### 4. **AboutPage** (`Frontend/src/features/content/pages/AboutPage.tsx`)
**Status: ✅ Fully Uses Settings**
- ✅ Uses `useCompanySettings()` hook
- ✅ Uses `settings.company_phone`, `settings.company_email`, `settings.company_address`
- ✅ No hardcoded fallbacks (uses `null` if not available)
---
### 5. **Policy Pages** (Privacy, Terms, Refunds, Cancellation, Accessibility, FAQ)
**Status: ✅ Uses Settings for Email**
- ✅ All use `useCompanySettings()` hook
- ✅ Use `settings.company_email` for contact links
- ✅ Only show email link if `settings.company_email` exists
**Pages:**
- `PrivacyPolicyPage.tsx`
- `TermsPage.tsx`
- `RefundsPolicyPage.tsx`
- `CancellationPolicyPage.tsx`
- `AccessibilityPage.tsx`
- `FAQPage.tsx`
---
### 6. **Customer Pages - Currency Usage**
**Status: ✅ All Use Currency Context**
All customer pages use `useFormatCurrency()` hook which uses `CurrencyContext`:
-`BookingDetailPage.tsx`
-`BookingSuccessPage.tsx`
-`MyBookingsPage.tsx`
-`RoomDetailPage.tsx`
-`FullPaymentPage.tsx`
-`PaymentConfirmationPage.tsx`
-`InvoicePage.tsx`
-`GroupBookingPage.tsx`
-`DashboardPage.tsx`
**Currency Source:** `CurrencyContext``localStorage.getItem('currency')` → Falls back to 'VND'
---
### 7. **Content Pages - Currency Usage**
**Status: ✅ All Use Currency Context**
-`HomePage.tsx` - Uses `useFormatCurrency()` for service prices
-`ServicesPage.tsx` - Uses `useFormatCurrency()` for service prices
-`ServiceDetailPage.tsx` - Uses `useFormatCurrency()` for service prices
---
### 8. **PaymentResultPage** (`Frontend/src/pages/customer/PaymentResultPage.tsx`)
**Status: ⚠️ Uses Settings with Hardcoded Fallbacks**
- ✅ Uses `useCompanySettings()` hook
-**Hardcoded fallbacks:**
- Email: `'support@hotel.com'`
- Phone: `'1900 xxxx'`
**Recommendation:** Remove hardcoded fallbacks.
---
### 9. **Auth Components**
**Status: ⚠️ Mixed Usage**
**ForgotPasswordModal:**
- ✅ Uses `settings.company_email || 'support@hotel.com'` (has fallback)
**Other Auth Components:**
- ❌ Only use placeholder text in form fields (acceptable for UX)
---
## ❌ Pages with Hardcoded Values
### 1. **PaymentConfirmationPage** (`Frontend/src/pages/customer/PaymentConfirmationPage.tsx`)
**Status: ❌ Hardcoded Bank Details**
**Hardcoded Values:**
- Bank: `'Vietcombank (VCB)'`
- Account Number: `'0123456789'`
- Account Holder: `'KHACH SAN ABC'`
**Recommendation:** Add bank details to Settings and make them configurable from admin.
---
## 📊 Summary Statistics
| Category | Total | Uses Settings | Hardcoded Fallbacks | Hardcoded Values |
|----------|-------|---------------|---------------------|------------------|
| **Email/Phone/Address** | 15+ pages | 12 pages | 3 pages | 0 pages |
| **Currency** | 12+ pages | 12 pages | 0 pages | 0 pages |
| **Bank Details** | 1 page | 0 pages | 0 pages | 1 page |
| **Logo** | 2 components | 2 components | 0 | 0 |
---
## 🔧 Issues Found
### High Priority
1. **PaymentConfirmationPage** - Bank details are hardcoded
- Should be added to Settings
- Should be configurable from admin
### Medium Priority
2. **Header Component** - Hardcoded fallback phone/email
- Should use empty string or null instead of fake values
3. **ContactPage** - Hardcoded fallback text instead of actual values
- Should show actual phone/email/address or nothing
4. **PaymentResultPage** - Hardcoded fallback support contact
- Should use settings or show nothing
### Low Priority
5. **Auth Components** - Placeholder text in forms (acceptable for UX)
---
## ✅ What's Working Well
1. **Currency System** - Fully centralized via `CurrencyContext`
- All pages use `useFormatCurrency()` hook
- Currency stored in localStorage
- Falls back to 'VND' if not set
2. **Footer Component** - Perfect implementation
- Uses settings without hardcoded fallbacks
- Shows nothing if settings not available
3. **AboutPage** - Perfect implementation
- Uses settings without hardcoded fallbacks
4. **Policy Pages** - Good implementation
- Only show email link if available
---
## 🛠️ Recommendations
### Immediate Actions
1. **Add Bank Details to Settings**
- Add fields: `bank_name`, `bank_account_number`, `bank_account_holder`
- Update PaymentConfirmationPage to use settings
- Add admin controls for bank details
2. **Remove Hardcoded Fallbacks**
- Header: Remove `'+1 (234) 567-890'` and `'info@luxuryhotel.com'`
- ContactPage: Remove fallback text, show actual values or nothing
- PaymentResultPage: Remove `'support@hotel.com'` and `'1900 xxxx'`
### Future Enhancements
3. **Currency Settings Integration**
- Consider adding default currency to Company Settings
- Allow admin to set default currency for the platform
4. **Settings Validation**
- Add validation to ensure critical settings (email, phone) are set
- Show warnings in admin if settings are missing
---
## 📝 Notes
- Currency is well-implemented via CurrencyContext
- Most pages correctly use `useCompanySettings()` hook
- Main issues are hardcoded fallback values that should be removed
- Bank details need to be added to settings system
- Placeholder text in form fields is acceptable and doesn't need changes