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
chat_working_hours_start: 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")
async def get_company_settings(
@@ -1452,6 +1456,10 @@ async def get_company_settings(
"tax_rate",
"chat_working_hours_start",
"chat_working_hours_end",
"bank_name",
"bank_account_number",
"bank_account_holder",
"bank_code",
]
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,
"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,
"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_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)
if request_data.chat_working_hours_end is not None:
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():
@@ -1581,7 +1601,7 @@ async def update_company_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(
SystemSettings.key == key
).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,
"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,
"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_by": updated_by,
}
@@ -1851,6 +1875,7 @@ class UpdateThemeSettingsRequest(BaseModel):
theme_primary_light: Optional[str] = None
theme_primary_dark: Optional[str] = None
theme_primary_accent: Optional[str] = None
theme_layout_mode: Optional[str] = None # 'dark' or 'light'
@router.get("/theme")
async def get_theme_settings(
@@ -1863,6 +1888,7 @@ async def get_theme_settings(
"theme_primary_light",
"theme_primary_dark",
"theme_primary_accent",
"theme_layout_mode",
]
settings_dict = {}
@@ -1893,6 +1919,7 @@ async def get_theme_settings(
"theme_primary_light": settings_dict.get("theme_primary_light", "#f5d76e"),
"theme_primary_dark": settings_dict.get("theme_primary_dark", "#c9a227"),
"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_by": updated_by,
}
@@ -1953,6 +1980,14 @@ async def update_theme_settings(
)
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
for key, value in db_settings.items():
setting = db.query(SystemSettings).filter(
@@ -1975,7 +2010,7 @@ async def update_theme_settings(
# Get 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(
SystemSettings.key == key
).first()
@@ -1988,6 +2023,7 @@ async def update_theme_settings(
"theme_primary_light": "#f5d76e",
"theme_primary_dark": "#c9a227",
"theme_primary_accent": "#e8c547",
"theme_layout_mode": "dark",
}
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_dark": updated_settings.get("theme_primary_dark", "#c9a227"),
"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_by": updated_by,
}

View File

@@ -307,22 +307,24 @@ export const notifyPayment = async (
export const generateQRCode = (
bookingNumber: string,
amount: number
amount: number,
bankCode?: string,
accountNumber?: string,
accountName?: string
): string => {
// If bank details are not provided, return empty string (QR code won't be generated)
if (!bankCode || !accountNumber || !accountName) {
return '';
}
const bankCode = 'VCB';
const accountNumber = '0123456789';
const accountName = 'KHACH SAN ABC';
const transferContent = bookingNumber;
// Generate QR code URL (using generic QR code service, not Vietnam-specific)
// Note: This is a placeholder - you may want to use a different QR code service
// that supports international bank transfers
const qrUrl =
`https://img.vietqr.io/image/${bankCode}-${accountNumber}-compact2.jpg?` +
`amount=${amount}&` +
`addInfo=${encodeURIComponent(transferContent)}&` +
`accountName=${encodeURIComponent(accountName)}`;
`https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=` +
encodeURIComponent(`Bank: ${bankCode}, Account: ${accountNumber}, Name: ${accountName}, Amount: ${amount}, Reference: ${transferContent}`);
return qrUrl;
};

View File

@@ -6,20 +6,26 @@ import {
Phone,
Mail,
Linkedin,
Twitter
Twitter,
Clock
} from 'lucide-react';
import * as LucideIcons from 'lucide-react';
import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
import { formatWorkingHours } from '../../../shared/utils/format';
import { getThemeTextClasses } from '../../../shared/utils/themeUtils';
const AboutPage: React.FC = () => {
const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [apiError, setApiError] = useState(false);
const [loading, setLoading] = useState(true);
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
useEffect(() => {
const fetchPageContent = async () => {
@@ -194,7 +200,9 @@ const AboutPage: React.FC = () => {
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-full blur-3xl opacity-40 animate-pulse"></div>
<div className="relative bg-gradient-to-br from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] p-6 rounded-2xl shadow-2xl shadow-[var(--luxury-gold)]/30">
<Hotel className="w-12 h-12 md:w-16 md:h-16 text-white drop-shadow-lg" />
{React.createElement(getIconComponent(pageContent?.about_hero_icon, Hotel), {
className: 'w-12 h-12 md:w-16 md:h-16 text-white drop-shadow-lg'
})}
</div>
</div>
</div>
@@ -445,13 +453,15 @@ const AboutPage: React.FC = () => {
<p className="text-[var(--luxury-gold)] font-medium mb-4 text-sm tracking-wide uppercase">{member.role}</p>
{member.bio && <p className="text-gray-600 text-sm mb-6 leading-relaxed font-light">{member.bio}</p>}
{member.social_links && (
<div className="flex gap-4 pt-4 border-t border-gray-100">
<div className={`flex gap-4 pt-4 border-t ${theme.theme_layout_mode === 'light' ? 'border-gray-200' : 'border-gray-700'}`}>
{member.social_links.linkedin && (
<a
href={member.social_links.linkedin}
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110"
className={`w-10 h-10 rounded-full ${theme.theme_layout_mode === 'light'
? 'bg-gray-100 text-gray-700'
: 'bg-gray-800 text-gray-300'} flex items-center justify-center hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110`}
>
<Linkedin className="w-5 h-5" />
</a>
@@ -461,7 +471,9 @@ const AboutPage: React.FC = () => {
href={member.social_links.twitter}
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110"
className={`w-10 h-10 rounded-full ${theme.theme_layout_mode === 'light'
? 'bg-gray-100 text-gray-700'
: 'bg-gray-800 text-gray-300'} flex items-center justify-center hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110`}
>
<Twitter className="w-5 h-5" />
</a>
@@ -614,7 +626,9 @@ const AboutPage: React.FC = () => {
{displayAddress && (
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2">
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
<MapPin className="w-8 h-8 text-white drop-shadow-md" />
{React.createElement(getIconComponent(pageContent?.about_contact_icons?.location, MapPin), {
className: 'w-8 h-8 text-white drop-shadow-md'
})}
</div>
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
Address
@@ -633,7 +647,9 @@ const AboutPage: React.FC = () => {
{displayPhone && (
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2" style={{ animationDelay: '0.1s' }}>
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
<Phone className="w-8 h-8 text-white drop-shadow-md" />
{React.createElement(getIconComponent(pageContent?.about_contact_icons?.phone, Phone), {
className: 'w-8 h-8 text-white drop-shadow-md'
})}
</div>
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
Phone
@@ -648,7 +664,9 @@ const AboutPage: React.FC = () => {
{displayEmail && (
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2" style={{ animationDelay: '0.2s' }}>
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
<Mail className="w-8 h-8 text-white drop-shadow-md" />
{React.createElement(getIconComponent(pageContent?.about_contact_icons?.email, Mail), {
className: 'w-8 h-8 text-white drop-shadow-md'
})}
</div>
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
Email
@@ -660,6 +678,19 @@ const AboutPage: React.FC = () => {
</p>
</div>
)}
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2" style={{ animationDelay: '0.3s' }}>
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
<Clock className="w-8 h-8 text-white drop-shadow-md" />
</div>
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
Chat Support Hours
</h3>
<p className="text-gray-600 font-light">
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
</p>
</div>
)}
</div>
)}
<div className="text-center">
@@ -668,7 +699,9 @@ const AboutPage: React.FC = () => {
className="group inline-flex items-center space-x-3 px-10 py-4 bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] text-white rounded-xl hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/40 transition-all duration-500 font-medium text-lg tracking-wide relative overflow-hidden"
>
<span className="relative z-10">Explore Our Rooms</span>
<Hotel className="w-5 h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300" />
{React.createElement(getIconComponent(pageContent?.about_learn_more_icon, Hotel), {
className: 'w-5 h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300'
})}
<div className="absolute inset-0 bg-gradient-to-r from-[var(--luxury-gold-light)] to-[var(--luxury-gold)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
</Link>
</div>

View File

@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
const AccessibilityPage: React.FC = () => {
const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true);
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
useEffect(() => {
const fetchPageContent = async () => {
try {
@@ -26,22 +33,26 @@ const AccessibilityPage: React.FC = () => {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent;
// Get theme-aware colors
const isLightMode = theme.theme_layout_mode === 'light';
const headingColor = isLightMode ? '#111827' : '#ffffff';
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
const accentColor = '#d4af37'; // Gold color for links and strong
const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => {
const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase();
const currentColor = htmlEl.style.color;
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = '#ffffff';
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = '#d4af37';
} else if (tagName === 'a') {
htmlEl.style.color = '#d4af37';
} else {
htmlEl.style.color = '#d1d5db';
}
// Override inline colors to use theme-aware colors
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = headingColor;
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = accentColor;
} else if (tagName === 'a') {
htmlEl.style.color = accentColor;
} else {
htmlEl.style.color = bodyColor;
}
});
@@ -83,7 +94,7 @@ const AccessibilityPage: React.FC = () => {
if (!pageContent) {
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -95,8 +106,8 @@ const AccessibilityPage: React.FC = () => {
>
<div className="text-center">
<Accessibility className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Accessibility</h1>
<p className="text-gray-400">This page is currently unavailable.</p>
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Accessibility</h1>
<p className={textClasses.muted}>This page is currently unavailable.</p>
<Link
to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
@@ -110,7 +121,7 @@ const AccessibilityPage: React.FC = () => {
}
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -133,38 +144,40 @@ const AccessibilityPage: React.FC = () => {
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
<Accessibility className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
{pageContent.title || 'Accessibility'}
</h1>
{pageContent.subtitle && (
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
{pageContent.subtitle}
</p>
)}
</div>
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
<div
className="prose prose-invert prose-lg max-w-none text-gray-300
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:font-elegant prose-headings:font-semibold
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:font-light prose-ul:my-4
prose-li:mb-2 prose-li:ml-4
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
style={{ color: '#d1d5db' }}
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
)}
/>
</div>
{settings.company_email && (
<div className="mt-8 text-center">
<p className="text-sm text-gray-400 font-light">
<p className={`text-sm ${textClasses.muted} font-light`}>
For accessibility inquiries, contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email}

View File

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

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 blogService, { BlogPost } from '../services/blogService';
import Loading from '../../../shared/components/Loading';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
import Pagination from '../../../shared/components/Pagination';
const BlogPage: React.FC = () => {
const { theme } = useTheme();
const [posts, setPosts] = useState<BlogPost[]>([]);
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
@@ -67,10 +70,16 @@ const BlogPage: React.FC = () => {
return <Loading />;
}
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
{/* Hero Section */}
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8 overflow-hidden relative">
<div className={`w-full ${heroBgClasses} pt-6 sm:pt-7 md:pt-8 overflow-hidden relative`}>
{/* Background Effects */}
<div className="absolute inset-0 opacity-10">
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
@@ -83,18 +92,20 @@ const BlogPage: React.FC = () => {
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
<div className="relative group">
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
<div className="relative p-2 sm:p-2.5 md:p-3 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300">
<div className={`relative p-2 sm:p-2.5 md:p-3 ${cardClasses} rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300`}>
<BookOpen className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg" />
</div>
</div>
</div>
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
<span className="bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
<span className={`${theme.theme_layout_mode === 'light'
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
Our Blog
</span>
</h1>
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
<p className="text-sm sm:text-base md:text-lg text-gray-300 font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4">
<p className={`text-sm sm:text-base md:text-lg ${textClasses.secondary} font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4`}>
Discover stories, insights, and updates from our luxury hotel
</p>
<div className="mt-4 flex items-center justify-center gap-2 text-[var(--luxury-gold)]/60">
@@ -125,7 +136,7 @@ const BlogPage: React.FC = () => {
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
className="w-full pl-14 pr-5 py-4 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] border border-[var(--luxury-gold)]/20 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light"
className={`w-full pl-14 pr-5 py-4 ${inputClasses} border border-[var(--luxury-gold)]/20 rounded-xl placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light`}
/>
</div>
</div>
@@ -139,7 +150,7 @@ const BlogPage: React.FC = () => {
{/* Results Count */}
{!loading && total > 0 && (
<div className="mb-8 text-gray-400 font-light text-sm">
<div className={`mb-8 ${textClasses.muted} font-light text-sm`}>
Showing {posts.length} of {total} posts
</div>
)}
@@ -150,8 +161,8 @@ const BlogPage: React.FC = () => {
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--luxury-gold)]/10 mb-6">
<BookOpen className="w-10 h-10 text-[var(--luxury-gold)]" />
</div>
<p className="text-gray-400 text-xl font-light">No blog posts found</p>
<p className="text-gray-500 text-sm mt-2">Try adjusting your search or filters</p>
<p className={`${textClasses.muted} text-xl font-light`}>No blog posts found</p>
<p className={`${textClasses.muted} text-sm mt-2`}>Try adjusting your search or filters</p>
</div>
) : (
<>
@@ -160,7 +171,7 @@ const BlogPage: React.FC = () => {
<Link
key={post.id}
to={`/blog/${post.slug}`}
className="group relative bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3"
className={`group relative ${cardClasses} rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3`}
style={{ animationDelay: `${index * 50}ms` }}
>
{/* Premium Glow Effects */}
@@ -204,15 +215,15 @@ const BlogPage: React.FC = () => {
))}
</div>
)}
<h2 className="text-2xl sm:text-3xl font-serif font-bold text-white mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight">
<h2 className={`text-2xl sm:text-3xl font-serif font-bold ${textClasses.primary} mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight`}>
{post.title}
</h2>
{post.excerpt && (
<p className="text-gray-300 text-base mb-6 line-clamp-3 font-light leading-relaxed">
<p className={`${textClasses.secondary} text-base mb-6 line-clamp-3 font-light leading-relaxed`}>
{post.excerpt}
</p>
)}
<div className="flex items-center justify-between text-sm text-gray-400 pt-6 border-t border-[var(--luxury-gold)]/20 mb-6">
<div className={`flex items-center justify-between text-sm ${textClasses.muted} pt-6 border-t border-[var(--luxury-gold)]/20 mb-6`}>
<div className="flex items-center gap-4">
{post.published_at && (
<div className="flex items-center gap-2">
@@ -259,10 +270,10 @@ const BlogPage: React.FC = () => {
<div className="lg:col-span-3">
{allTags.length > 0 && (
<div className="sticky top-8">
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 backdrop-blur-xl shadow-2xl">
<div className={`${cardClasses} rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 backdrop-blur-xl shadow-2xl`}>
<div className="flex items-center gap-3 mb-6 pb-6 border-b border-[var(--luxury-gold)]/20">
<div className="w-1 h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
<h3 className="text-xl font-serif font-bold text-white">Filter by Tags</h3>
<h3 className={`text-xl font-serif font-bold ${textClasses.primary}`}>Filter by Tags</h3>
</div>
<div className="space-y-3">
<button
@@ -273,7 +284,7 @@ const BlogPage: React.FC = () => {
className={`group relative w-full text-left px-5 py-4 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
selectedTag === null
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] hover:bg-[#1a1a1a]'
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
}`}
>
<span className="relative z-10 flex items-center gap-2">
@@ -294,7 +305,7 @@ const BlogPage: React.FC = () => {
className={`group relative w-full text-left px-5 py-4 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
selectedTag === tag
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] hover:bg-[#1a1a1a]'
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
}`}
>
<span className="relative z-10 flex items-center gap-2">

View File

@@ -5,12 +5,19 @@ import pageContentService, { PageContent } from '../services/pageContentService'
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
const CancellationPolicyPage: React.FC = () => {
const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true);
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
useEffect(() => {
const fetchPageContent = async () => {
try {
@@ -26,22 +33,27 @@ const CancellationPolicyPage: React.FC = () => {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent;
// Get theme-aware colors
const isLightMode = theme.theme_layout_mode === 'light';
const headingColor = isLightMode ? '#111827' : '#ffffff';
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
const accentColor = '#d4af37'; // Gold color for links and strong
const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => {
const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase();
const currentColor = htmlEl.style.color;
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = '#ffffff';
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = '#d4af37';
} else if (tagName === 'a') {
htmlEl.style.color = '#d4af37';
} else {
htmlEl.style.color = '#d1d5db';
}
// Override inline colors to use theme-aware colors
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = headingColor;
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = accentColor;
} else if (tagName === 'a') {
htmlEl.style.color = accentColor;
} else {
htmlEl.style.color = bodyColor;
}
});
@@ -83,7 +95,7 @@ const CancellationPolicyPage: React.FC = () => {
if (!pageContent) {
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -95,8 +107,8 @@ const CancellationPolicyPage: React.FC = () => {
>
<div className="text-center">
<XCircle className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Cancellation Policy</h1>
<p className="text-gray-400">This page is currently unavailable.</p>
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Cancellation Policy</h1>
<p className={textClasses.muted}>This page is currently unavailable.</p>
<Link
to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
@@ -110,7 +122,7 @@ const CancellationPolicyPage: React.FC = () => {
}
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -133,38 +145,42 @@ const CancellationPolicyPage: React.FC = () => {
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
<XCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
{pageContent.title || 'Cancellation Policy'}
</h1>
{pageContent.subtitle && (
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
{pageContent.subtitle}
</p>
)}
</div>
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
<div
className="prose prose-invert prose-lg max-w-none text-gray-300
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:font-elegant prose-headings:font-semibold
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:font-light prose-ul:my-4
prose-li:mb-2 prose-li:ml-4
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
style={{ color: '#d1d5db' }}
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
style={{
color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db',
}}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
)}
/>
</div>
{settings.company_email && (
<div className="mt-8 text-center">
<p className="text-sm text-gray-400 font-light">
<p className={`text-sm ${textClasses.muted} font-light`}>
For questions about cancellations, contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email}

View File

@@ -1,18 +1,41 @@
import React, { useState, useEffect } from 'react';
import { Mail, Phone, MapPin, Send, User, MessageSquare } from 'lucide-react';
import { Mail, Phone, MapPin, Send, User, MessageSquare, Clock } from 'lucide-react';
import * as LucideIcons from 'lucide-react';
import { submitContactForm } from '../services/contactService';
import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { toast } from 'react-toastify';
import Recaptcha from '../../../shared/components/Recaptcha';
import { recaptchaService } from '../../../features/system/services/systemSettingsService';
import ChatWidget from '../../notifications/components/ChatWidget';
import { useAntibotForm } from '../../../features/auth/hooks/useAntibotForm';
import HoneypotField from '../../../shared/components/HoneypotField';
import { formatWorkingHours } from '../../../shared/utils/format';
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
// Helper function to get icon component from icon name (handles both PascalCase and lowercase)
const getIconComponent = (iconName?: string, fallback: any = Mail) => {
if (!iconName) return fallback;
// Try direct match first (for PascalCase names)
if ((LucideIcons as any)[iconName]) {
return (LucideIcons as any)[iconName];
}
// Convert to PascalCase (capitalize first letter)
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
if ((LucideIcons as any)[pascalCaseName]) {
return (LucideIcons as any)[pascalCaseName];
}
return fallback;
};
const ContactPage: React.FC = () => {
const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [formData, setFormData] = useState({
name: '',
@@ -153,9 +176,9 @@ const ContactPage: React.FC = () => {
}, []);
const displayPhone = settings.company_phone || 'Available 24/7 for your convenience';
const displayEmail = settings.company_email || "We'll respond within 24 hours";
const displayAddress = settings.company_address || 'Visit us at our hotel reception';
const displayPhone = settings.company_phone || null;
const displayEmail = settings.company_email || null;
const displayAddress = settings.company_address || null;
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
@@ -171,10 +194,16 @@ const ContactPage: React.FC = () => {
}
};
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
{}
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8 overflow-hidden relative">
<div className={`w-full ${heroBgClasses} pt-6 sm:pt-7 md:pt-8 overflow-hidden relative`}>
{}
<div className="absolute inset-0 opacity-10">
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
@@ -187,18 +216,22 @@ const ContactPage: React.FC = () => {
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
<div className="relative group">
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
<div className="relative p-2 sm:p-2.5 md:p-3 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300">
<Mail className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg" />
<div className={`relative p-2 sm:p-2.5 md:p-3 ${cardClasses} rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300`}>
{React.createElement(getIconComponent(pageContent?.contact_icons?.hero, Mail), {
className: 'w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg'
})}
</div>
</div>
</div>
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
<span className="bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
<h1 className={`text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2`}>
<span className={theme.theme_layout_mode === 'light'
? "bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900 bg-clip-text text-transparent"
: "bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent"}>
{pageContent?.title || 'Contact Us'}
</span>
</h1>
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
<p className="text-sm sm:text-base md:text-lg text-gray-300 font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4">
<p className={`text-sm sm:text-base md:text-lg ${textClasses.secondary} font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4`}>
{pageContent?.subtitle || pageContent?.description || "Experience the pinnacle of hospitality. We're here to make your stay extraordinary."}
</p>
</div>
@@ -212,18 +245,18 @@ const ContactPage: React.FC = () => {
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 sm:gap-5 md:gap-6 lg:gap-7 xl:gap-8 2xl:gap-10 max-w-7xl mx-auto">
{}
<div className="lg:col-span-4">
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a]
<div className={`${cardClasses}
rounded-xl sm:rounded-2xl border-2 border-[var(--luxury-gold)]/30 p-5 sm:p-6 md:p-8 lg:p-10
shadow-2xl shadow-[var(--luxury-gold)]/10 backdrop-blur-xl
relative overflow-hidden h-full group hover:border-[var(--luxury-gold)]/50 transition-all duration-500">
relative overflow-hidden h-full group hover:border-[var(--luxury-gold)]/50 transition-all duration-500`}>
{}
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)]/5 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div className="relative z-10">
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8">
<div className="w-0.5 sm:w-1 h-6 sm:h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
<h2 className="text-xl sm:text-2xl md:text-3xl font-serif font-semibold
text-white tracking-tight">
<h2 className={`text-xl sm:text-2xl md:text-3xl font-serif font-semibold
${textClasses.primary} tracking-tight`}>
Get in Touch
</h2>
</div>
@@ -231,11 +264,13 @@ const ContactPage: React.FC = () => {
<div className="space-y-5 sm:space-y-6 md:space-y-7">
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
<Mail className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
{React.createElement(getIconComponent(pageContent?.contact_icons?.email, Mail), {
className: 'w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg'
})}
</div>
<div>
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Email</h3>
<a href={`mailto:${displayEmail}`} className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors">
<a href={`mailto:${displayEmail}`} className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors`}>
{displayEmail}
</a>
</div>
@@ -243,11 +278,13 @@ const ContactPage: React.FC = () => {
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
<Phone className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
{React.createElement(getIconComponent(pageContent?.contact_icons?.phone, Phone), {
className: 'w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg'
})}
</div>
<div>
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Phone</h3>
<a href={`tel:${displayPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`} className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors">
<a href={`tel:${displayPhone?.replace(/\s+/g, '').replace(/[()]/g, '') || ''}`} className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors`}>
{displayPhone}
</a>
</div>
@@ -255,15 +292,31 @@ const ContactPage: React.FC = () => {
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
<MapPin className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
{React.createElement(getIconComponent(pageContent?.contact_icons?.location, MapPin), {
className: 'w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg'
})}
</div>
<div>
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Location</h3>
<p className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed whitespace-pre-line">
<p className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed whitespace-pre-line`}>
{displayAddress}
</p>
</div>
</div>
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
<Clock className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
</div>
<div>
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Chat Support Hours</h3>
<p className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed`}>
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
</p>
</div>
</div>
)}
</div>
{}
@@ -289,7 +342,7 @@ const ContactPage: React.FC = () => {
)}
<div className="mt-5 sm:mt-6 md:mt-7 pt-5 sm:pt-6 md:pt-7 border-t border-[var(--luxury-gold)]/30">
<p className="text-gray-400 font-light text-xs sm:text-sm leading-relaxed tracking-wide">
<p className={`${textClasses.muted} font-light text-xs sm:text-sm leading-relaxed tracking-wide`}>
{pageContent?.content || "Our team is here to help you with any questions about your stay, bookings, or special requests. We're committed to exceeding your expectations."}
</p>
</div>
@@ -299,10 +352,10 @@ const ContactPage: React.FC = () => {
{}
<div className="lg:col-span-8">
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a]
<div className={`${cardClasses}
rounded-xl sm:rounded-2xl border-2 border-[var(--luxury-gold)]/30 p-5 sm:p-6 md:p-8 lg:p-10
shadow-2xl shadow-[var(--luxury-gold)]/10 backdrop-blur-xl
relative overflow-hidden">
relative overflow-hidden`}>
{}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-0 right-0 w-48 sm:w-64 md:w-96 h-48 sm:h-64 md:h-96 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
@@ -311,8 +364,8 @@ const ContactPage: React.FC = () => {
<div className="relative z-10">
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8">
<div className="w-0.5 sm:w-1 h-6 sm:h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
<h2 className="text-xl sm:text-2xl md:text-3xl font-serif font-semibold
text-white tracking-tight">
<h2 className={`text-xl sm:text-2xl md:text-3xl font-serif font-semibold
${textClasses.primary} tracking-tight`}>
Send Us a Message
</h2>
</div>
@@ -328,9 +381,11 @@ const ContactPage: React.FC = () => {
)}
{}
<div>
<label htmlFor="name" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
<label htmlFor="name" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
<span className="flex items-center gap-1.5 sm:gap-2">
<User className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
{React.createElement(getIconComponent(pageContent?.contact_icons?.name_field, User), {
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
})}
Full Name <span className="text-[var(--luxury-gold)] font-semibold">*</span>
</span>
</label>
@@ -340,8 +395,8 @@ const ContactPage: React.FC = () => {
name="name"
value={formData.name}
onChange={handleChange}
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
text-white text-sm sm:text-base placeholder-gray-500/60
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
text-sm sm:text-base placeholder-gray-500/60
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
${errors.name ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
@@ -356,9 +411,11 @@ const ContactPage: React.FC = () => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-5 md:gap-6 lg:gap-7">
{}
<div>
<label htmlFor="email" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
<label htmlFor="email" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
<span className="flex items-center gap-1.5 sm:gap-2">
<Mail className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
{React.createElement(getIconComponent(pageContent?.contact_icons?.email_field, Mail), {
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
})}
Email <span className="text-[var(--luxury-gold)] font-semibold">*</span>
</span>
</label>
@@ -368,8 +425,8 @@ const ContactPage: React.FC = () => {
name="email"
value={formData.email}
onChange={handleChange}
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
text-white text-sm sm:text-base placeholder-gray-500/60
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
text-sm sm:text-base placeholder-gray-500/60
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
${errors.email ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
@@ -382,10 +439,12 @@ const ContactPage: React.FC = () => {
{}
<div>
<label htmlFor="phone" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
<label htmlFor="phone" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
<span className="flex items-center gap-1.5 sm:gap-2">
<Phone className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
Phone <span className="text-gray-500 text-xs">(Optional)</span>
{React.createElement(getIconComponent(pageContent?.contact_icons?.phone_field, Phone), {
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
})}
Phone <span className={`${textClasses.muted} text-xs`}>(Optional)</span>
</span>
</label>
<input
@@ -394,10 +453,10 @@ const ContactPage: React.FC = () => {
name="phone"
value={formData.phone}
onChange={handleChange}
className="w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 border-[var(--luxury-gold)]/30 rounded-lg
text-white text-sm sm:text-base placeholder-gray-500/60
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 border-[var(--luxury-gold)]/30 rounded-lg
text-sm sm:text-base placeholder-gray-500/60
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
transition-all duration-300 hover:border-[var(--luxury-gold)]/40"
transition-all duration-300 hover:border-[var(--luxury-gold)]/40`}
placeholder="+1 (555) 123-4567"
/>
</div>
@@ -405,9 +464,11 @@ const ContactPage: React.FC = () => {
{}
<div>
<label htmlFor="subject" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
<label htmlFor="subject" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
<span className="flex items-center gap-1.5 sm:gap-2">
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
{React.createElement(getIconComponent(pageContent?.contact_icons?.subject_field, MessageSquare), {
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
})}
Subject <span className="text-[var(--luxury-gold)] font-semibold">*</span>
</span>
</label>
@@ -417,8 +478,8 @@ const ContactPage: React.FC = () => {
name="subject"
value={formData.subject}
onChange={handleChange}
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
text-white text-sm sm:text-base placeholder-gray-500/60
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
text-sm sm:text-base placeholder-gray-500/60
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
${errors.subject ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
@@ -431,9 +492,11 @@ const ContactPage: React.FC = () => {
{}
<div>
<label htmlFor="message" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
<label htmlFor="message" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
<span className="flex items-center gap-1.5 sm:gap-2">
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
{React.createElement(getIconComponent(pageContent?.contact_icons?.message_field, MessageSquare), {
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
})}
Message <span className="text-[var(--luxury-gold)] font-semibold">*</span>
</span>
</label>
@@ -443,8 +506,8 @@ const ContactPage: React.FC = () => {
value={formData.message}
onChange={handleChange}
rows={6}
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
text-white text-sm sm:text-base placeholder-gray-500/60 resize-none
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
text-sm sm:text-base placeholder-gray-500/60 resize-none
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
${errors.message ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
@@ -494,7 +557,9 @@ const ContactPage: React.FC = () => {
</>
) : (
<>
<Send className="w-4 h-4 sm:w-5 sm:h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300" />
{React.createElement(getIconComponent(pageContent?.contact_icons?.submit_button, Send), {
className: 'w-4 h-4 sm:w-5 sm:h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300'
})}
<span className="relative z-10">Send Message</span>
</>
)}

View File

@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
const FAQPage: React.FC = () => {
const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true);
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
useEffect(() => {
const fetchPageContent = async () => {
try {
@@ -26,22 +33,26 @@ const FAQPage: React.FC = () => {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent;
// Get theme-aware colors
const isLightMode = theme.theme_layout_mode === 'light';
const headingColor = isLightMode ? '#111827' : '#ffffff';
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
const accentColor = '#d4af37'; // Gold color for links and strong
// Override inline colors to use theme-aware colors
const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => {
const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase();
const currentColor = htmlEl.style.color;
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = '#ffffff';
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = '#d4af37';
} else if (tagName === 'a') {
htmlEl.style.color = '#d4af37';
} else {
htmlEl.style.color = '#d1d5db';
}
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = headingColor;
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = accentColor;
} else if (tagName === 'a') {
htmlEl.style.color = accentColor;
} else {
htmlEl.style.color = bodyColor;
}
});
@@ -83,7 +94,7 @@ const FAQPage: React.FC = () => {
if (!pageContent) {
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -95,8 +106,8 @@ const FAQPage: React.FC = () => {
>
<div className="text-center">
<HelpCircle className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Frequently Asked Questions</h1>
<p className="text-gray-400">This page is currently unavailable.</p>
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Frequently Asked Questions</h1>
<p className={textClasses.muted}>This page is currently unavailable.</p>
<Link
to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
@@ -110,7 +121,7 @@ const FAQPage: React.FC = () => {
}
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -133,38 +144,40 @@ const FAQPage: React.FC = () => {
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
<HelpCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900 bg-clip-text text-transparent'
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent'}`}>
{pageContent.title || 'Frequently Asked Questions'}
</h1>
{pageContent.subtitle && (
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
{pageContent.subtitle}
</p>
)}
</div>
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
<div
className="prose prose-invert prose-lg max-w-none text-gray-300
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:font-elegant prose-headings:font-semibold
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:font-light prose-ul:my-4
prose-li:mb-2 prose-li:ml-4
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
style={{ color: '#d1d5db' }}
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
)}
/>
</div>
{settings.company_email && (
<div className="mt-8 text-center">
<p className="text-sm text-gray-400 font-light">
<p className={`text-sm ${textClasses.muted} font-light`}>
Still have questions? Contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email}

View File

@@ -29,6 +29,24 @@ import type { Room } from '../../rooms/services/roomService';
import type { PageContent } from '../services/pageContentService';
import type { Service } from '../../hotel_services/services/serviceService';
// Helper function to get icon component from icon name (handles both PascalCase and lowercase)
const getIconComponent = (iconName?: string) => {
if (!iconName) return null;
// Try direct match first (for PascalCase names)
if ((LucideIcons as any)[iconName]) {
return (LucideIcons as any)[iconName];
}
// Convert to PascalCase (capitalize first letter)
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
if ((LucideIcons as any)[pascalCaseName]) {
return (LucideIcons as any)[pascalCaseName];
}
return null;
};
const HomePage: React.FC = () => {
const navigate = useNavigate();
const { formatCurrency } = useFormatCurrency();
@@ -1081,31 +1099,34 @@ const HomePage: React.FC = () => {
)}
<div className="relative grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6 lg:gap-8">
{pageContent.stats.map((stat, index) => (
<div key={`stat-${index}-${stat.label || index}`} className="text-center group relative">
{stat?.icon && (
<div className="mb-3 md:mb-4 group-hover:scale-110 transition-transform duration-300 flex items-center justify-center">
{stat.icon && (LucideIcons as any)[stat.icon] ? (
React.createElement((LucideIcons as any)[stat.icon], {
className: 'w-8 h-8 md:w-10 md:h-10 text-[var(--luxury-gold)] drop-shadow-md'
})
) : (
<span className="text-3xl md:text-4xl">{stat.icon}</span>
)}
</div>
)}
{stat?.number && (
<div className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--luxury-gold)] mb-1 md:mb-2 font-serif tracking-tight">
{stat.number}
</div>
)}
{stat?.label && (
<div className="text-gray-300 text-xs sm:text-sm md:text-base font-light tracking-wider uppercase">
{stat.label}
</div>
)}
</div>
))}
{pageContent.stats.map((stat, index) => {
const IconComponent = stat?.icon ? getIconComponent(stat.icon) : null;
return (
<div key={`stat-${index}-${stat.label || index}`} className="text-center group relative">
{stat?.icon && (
<div className="mb-3 md:mb-4 group-hover:scale-110 transition-transform duration-300 flex items-center justify-center">
{IconComponent ? (
React.createElement(IconComponent, {
className: 'w-8 h-8 md:w-10 md:h-10 text-[var(--luxury-gold)] drop-shadow-md'
})
) : (
<span className="text-3xl md:text-4xl">✨</span>
)}
</div>
)}
{stat?.number && (
<div className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--luxury-gold)] mb-1 md:mb-2 font-serif tracking-tight">
{stat.number}
</div>
)}
{stat?.label && (
<div className="text-gray-300 text-xs sm:text-sm md:text-base font-light tracking-wider uppercase">
{stat.label}
</div>
)}
</div>
);
})}
</div>
</div>
</section>

View File

@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
const PrivacyPolicyPage: React.FC = () => {
const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true);
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
useEffect(() => {
const fetchPageContent = async () => {
try {
@@ -28,24 +35,26 @@ const PrivacyPolicyPage: React.FC = () => {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent;
// Add color styles to elements that don't have them
// Get theme-aware colors
const isLightMode = theme.theme_layout_mode === 'light';
const headingColor = isLightMode ? '#111827' : '#ffffff';
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
const accentColor = '#d4af37'; // Gold color for links and strong
// Override inline colors to use theme-aware colors
const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => {
const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase();
const currentColor = htmlEl.style.color;
// Only add color if not already set
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = '#ffffff';
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = '#d4af37';
} else if (tagName === 'a') {
htmlEl.style.color = '#d4af37';
} else {
htmlEl.style.color = '#d1d5db';
}
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = headingColor;
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = accentColor;
} else if (tagName === 'a') {
htmlEl.style.color = accentColor;
} else {
htmlEl.style.color = bodyColor;
}
});
@@ -88,7 +97,7 @@ const PrivacyPolicyPage: React.FC = () => {
if (!pageContent) {
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -100,8 +109,8 @@ const PrivacyPolicyPage: React.FC = () => {
>
<div className="text-center">
<Shield className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Privacy Policy</h1>
<p className="text-gray-400">This page is currently unavailable.</p>
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Privacy Policy</h1>
<p className={textClasses.muted}>This page is currently unavailable.</p>
<Link
to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
@@ -115,7 +124,7 @@ const PrivacyPolicyPage: React.FC = () => {
}
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -140,32 +149,34 @@ const PrivacyPolicyPage: React.FC = () => {
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
<Shield className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
{pageContent.title || 'Privacy Policy'}
</h1>
{pageContent.subtitle && (
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
{pageContent.subtitle}
</p>
)}
</div>
{/* Content */}
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
<div
className="prose prose-invert prose-lg max-w-none text-gray-300
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:font-elegant prose-headings:font-semibold
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:font-light prose-ul:my-4
prose-li:mb-2 prose-li:ml-4
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
style={{ color: '#d1d5db' }}
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
)}
/>
</div>
@@ -174,7 +185,7 @@ const PrivacyPolicyPage: React.FC = () => {
<div className="mt-8 space-y-4">
{settings.company_email && (
<div className="text-center">
<p className="text-sm text-gray-400 font-light">
<p className={`text-sm ${textClasses.muted} font-light`}>
For questions about this policy, contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email}

View File

@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
const RefundsPolicyPage: React.FC = () => {
const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true);
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
useEffect(() => {
const fetchPageContent = async () => {
try {
@@ -28,24 +35,26 @@ const RefundsPolicyPage: React.FC = () => {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent;
// Add color styles to elements that don't have them
// Get theme-aware colors
const isLightMode = theme.theme_layout_mode === 'light';
const headingColor = isLightMode ? '#111827' : '#ffffff';
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
const accentColor = '#d4af37'; // Gold color for links and strong
// Override inline colors to use theme-aware colors
const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => {
const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase();
const currentColor = htmlEl.style.color;
// Only add color if not already set
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = '#ffffff';
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = '#d4af37';
} else if (tagName === 'a') {
htmlEl.style.color = '#d4af37';
} else {
htmlEl.style.color = '#d1d5db';
}
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = headingColor;
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = accentColor;
} else if (tagName === 'a') {
htmlEl.style.color = accentColor;
} else {
htmlEl.style.color = bodyColor;
}
});
@@ -88,7 +97,7 @@ const RefundsPolicyPage: React.FC = () => {
if (!pageContent) {
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -100,8 +109,8 @@ const RefundsPolicyPage: React.FC = () => {
>
<div className="text-center">
<RefreshCw className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Refunds Policy</h1>
<p className="text-gray-400">This page is currently unavailable.</p>
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Refunds Policy</h1>
<p className={textClasses.muted}>This page is currently unavailable.</p>
<Link
to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
@@ -115,7 +124,7 @@ const RefundsPolicyPage: React.FC = () => {
}
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -140,32 +149,34 @@ const RefundsPolicyPage: React.FC = () => {
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
<RefreshCw className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
{pageContent.title || 'Refunds Policy'}
</h1>
{pageContent.subtitle && (
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
{pageContent.subtitle}
</p>
)}
</div>
{/* Content */}
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
<div
className="prose prose-invert prose-lg max-w-none text-gray-300
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:font-elegant prose-headings:font-semibold
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:font-light prose-ul:my-4
prose-li:mb-2 prose-li:ml-4
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
style={{ color: '#d1d5db' }}
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
)}
/>
</div>
@@ -173,7 +184,7 @@ const RefundsPolicyPage: React.FC = () => {
{/* Footer Note */}
{settings.company_email && (
<div className="mt-8 text-center">
<p className="text-sm text-gray-400 font-light">
<p className={`text-sm ${textClasses.muted} font-light`}>
For refund inquiries, contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email}

View File

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

View File

@@ -6,8 +6,29 @@ import pageContentService, { PageContent } from '../services/pageContentService'
import serviceService, { Service } from '../../hotel_services/services/serviceService';
import Loading from '../../../shared/components/Loading';
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
// Helper function to get icon component from icon name (handles both PascalCase and lowercase)
const getIconComponent = (iconName?: string, fallback: any = Award) => {
if (!iconName) return fallback;
// Try direct match first (for PascalCase names)
if ((LucideIcons as any)[iconName]) {
return (LucideIcons as any)[iconName];
}
// Convert to PascalCase (capitalize first letter)
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
if ((LucideIcons as any)[pascalCaseName]) {
return (LucideIcons as any)[pascalCaseName];
}
return fallback;
};
const ServicesPage: React.FC = () => {
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [hotelServices, setHotelServices] = useState<Service[]>([]);
const [loading, setLoading] = useState(true);
@@ -156,10 +177,16 @@ const ServicesPage: React.FC = () => {
return <Loading />;
}
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
{/* Hero Section */}
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8 overflow-hidden relative">
<div className={`w-full ${heroBgClasses} pt-6 sm:pt-7 md:pt-8 overflow-hidden relative`}>
{/* Background Effects */}
<div className="absolute inset-0 opacity-10">
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
@@ -172,18 +199,20 @@ const ServicesPage: React.FC = () => {
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
<div className="relative group">
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
<div className="relative p-2 sm:p-2.5 md:p-3 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300">
<div className={`relative p-2 sm:p-2.5 md:p-3 ${cardClasses} rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300`}>
<Award className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg" />
</div>
</div>
</div>
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
<span className="bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
<span className={`${theme.theme_layout_mode === 'light'
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
{pageContent?.luxury_services_section_title || 'Our Services'}
</span>
</h1>
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
<p className="text-sm sm:text-base md:text-lg text-gray-300 font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4">
<p className={`text-sm sm:text-base md:text-lg ${textClasses.secondary} font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4`}>
{pageContent?.luxury_services_section_subtitle || 'Discover our premium services designed to enhance your stay'}
</p>
<div className="mt-4 flex items-center justify-center gap-2 text-[var(--luxury-gold)]/60">
@@ -211,7 +240,7 @@ const ServicesPage: React.FC = () => {
placeholder="Search services..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-14 pr-5 py-4 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] border border-[var(--luxury-gold)]/20 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light"
className={`w-full pl-14 pr-5 py-4 ${inputClasses} border border-[var(--luxury-gold)]/20 rounded-xl placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light`}
/>
</div>
</div>
@@ -221,13 +250,13 @@ const ServicesPage: React.FC = () => {
{/* Categories Filter - Top Center */}
{allCategories.length > 0 && (
<div className="mb-12 flex justify-center">
<div className="inline-flex flex-wrap items-center gap-3 bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-2xl border-2 border-[var(--luxury-gold)]/20 p-4 backdrop-blur-xl shadow-2xl">
<div className={`inline-flex flex-wrap items-center gap-3 ${cardClasses} rounded-2xl border-2 border-[var(--luxury-gold)]/20 p-4 backdrop-blur-xl shadow-2xl`}>
<button
onClick={() => setSelectedCategory(null)}
className={`group relative px-6 py-3 rounded-xl text-sm font-medium transition-all duration-300 overflow-hidden ${
selectedCategory === null
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)]'
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
}`}
>
<span className="relative z-10 flex items-center gap-2">
@@ -242,7 +271,7 @@ const ServicesPage: React.FC = () => {
className={`group relative px-6 py-3 rounded-xl text-sm font-medium transition-all duration-300 overflow-hidden ${
selectedCategory === category
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)]'
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
}`}
>
<span className="relative z-10 flex items-center gap-2">
@@ -259,7 +288,7 @@ const ServicesPage: React.FC = () => {
<div className="flex flex-col items-center">
{/* Results Count */}
{!loading && filteredServices.length > 0 && (
<div className="mb-8 text-gray-400 font-light text-sm text-center">
<div className={`mb-8 ${textClasses.muted} font-light text-sm text-center`}>
Showing {filteredServices.length} of {allServices.length} services
</div>
)}
@@ -270,8 +299,8 @@ const ServicesPage: React.FC = () => {
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--luxury-gold)]/10 mb-6">
<Award className="w-10 h-10 text-[var(--luxury-gold)]" />
</div>
<p className="text-gray-400 text-xl font-light">No services found</p>
<p className="text-gray-500 text-sm mt-2">Try adjusting your search or filters</p>
<p className={`${textClasses.muted} text-xl font-light`}>No services found</p>
<p className={`${textClasses.muted} text-sm mt-2`}>Try adjusting your search or filters</p>
</div>
) : (
<>
@@ -288,7 +317,7 @@ const ServicesPage: React.FC = () => {
<Link
key={service.id}
to={`/services/${serviceSlug}`}
className="group relative bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3 block"
className={`group relative ${cardClasses} rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3 block`}
style={{ animationDelay: `${index * 50}ms` }}
>
{/* Premium Glow Effects */}
@@ -320,7 +349,7 @@ const ServicesPage: React.FC = () => {
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
</div>
) : (
<div className="h-48 sm:h-56 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] flex items-center justify-center p-8">
<div className={`h-48 sm:h-56 ${cardClasses} flex items-center justify-center p-8`}>
{service.icon && (LucideIcons as any)[service.icon] ? (
<div className="relative">
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div>
@@ -331,7 +360,12 @@ const ServicesPage: React.FC = () => {
) : (
<div className="relative">
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div>
<Award className="w-16 h-16 sm:w-20 sm:h-20 text-[var(--luxury-gold)] relative z-10 drop-shadow-lg" />
{React.createElement(
getIconComponent(pageContent?.services_fallback_icon, Award),
{
className: 'w-16 h-16 sm:w-20 sm:h-20 text-[var(--luxury-gold)] relative z-10 drop-shadow-lg'
}
)}
</div>
)}
</div>
@@ -345,11 +379,11 @@ const ServicesPage: React.FC = () => {
</span>
</div>
)}
<h2 className="text-2xl sm:text-3xl font-serif font-bold text-white mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight">
<h2 className={`text-2xl sm:text-3xl font-serif font-bold ${textClasses.primary} mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight`}>
{service.title}
</h2>
{service.description && (
<p className="text-gray-300 text-base mb-6 line-clamp-3 font-light leading-relaxed">
<p className={`${textClasses.secondary} text-base mb-6 line-clamp-3 font-light leading-relaxed`}>
{service.description}
</p>
)}
@@ -360,7 +394,7 @@ const ServicesPage: React.FC = () => {
{formatCurrency(service.price)}
</span>
{service.unit && (
<span className="text-sm text-gray-400 font-light">
<span className={`text-sm ${textClasses.muted} font-light`}>
/ {service.unit}
</span>
)}

View File

@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
import pageContentService from '../services/pageContentService';
import type { PageContent } from '../services/pageContentService';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { useTheme } from '../../../shared/contexts/ThemeContext';
import Loading from '../../../shared/components/Loading';
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
const TermsPage: React.FC = () => {
const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [loading, setLoading] = useState(true);
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
useEffect(() => {
const fetchPageContent = async () => {
try {
@@ -28,24 +35,26 @@ const TermsPage: React.FC = () => {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent;
// Add color styles to elements that don't have them
// Get theme-aware colors
const isLightMode = theme.theme_layout_mode === 'light';
const headingColor = isLightMode ? '#111827' : '#ffffff';
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
const accentColor = '#d4af37'; // Gold color for links and strong
// Override inline colors to use theme-aware colors
const allElements = tempDiv.querySelectorAll('*');
allElements.forEach((el) => {
const htmlEl = el as HTMLElement;
const tagName = htmlEl.tagName.toLowerCase();
const currentColor = htmlEl.style.color;
// Only add color if not already set
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = '#ffffff';
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = '#d4af37';
} else if (tagName === 'a') {
htmlEl.style.color = '#d4af37';
} else {
htmlEl.style.color = '#d1d5db';
}
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
htmlEl.style.color = headingColor;
} else if (['strong', 'b'].includes(tagName)) {
htmlEl.style.color = accentColor;
} else if (tagName === 'a') {
htmlEl.style.color = accentColor;
} else {
htmlEl.style.color = bodyColor;
}
});
@@ -88,7 +97,7 @@ const TermsPage: React.FC = () => {
if (!pageContent) {
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -100,8 +109,8 @@ const TermsPage: React.FC = () => {
>
<div className="text-center">
<Scale className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Terms & Conditions</h1>
<p className="text-gray-400">This page is currently unavailable.</p>
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Terms & Conditions</h1>
<p className={textClasses.muted}>This page is currently unavailable.</p>
<Link
to="/"
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
@@ -115,7 +124,7 @@ const TermsPage: React.FC = () => {
}
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
style={{
marginLeft: 'calc(50% - 50vw)',
marginRight: 'calc(50% - 50vw)',
@@ -140,32 +149,34 @@ const TermsPage: React.FC = () => {
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
<Scale className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
{pageContent.title || 'Terms & Conditions'}
</h1>
{pageContent.subtitle && (
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
{pageContent.subtitle}
</p>
)}
</div>
{/* Content */}
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
<div
className="prose prose-invert prose-lg max-w-none text-gray-300
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
prose-headings:font-elegant prose-headings:font-semibold
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
prose-ul:font-light prose-ul:my-4
prose-li:mb-2 prose-li:ml-4
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
style={{ color: '#d1d5db' }}
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
dangerouslySetInnerHTML={createSanitizedHtml(
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
)}
/>
</div>
@@ -173,7 +184,7 @@ const TermsPage: React.FC = () => {
{/* Footer Note */}
{settings.company_email && (
<div className="mt-8 text-center">
<p className="text-sm text-gray-400 font-light">
<p className={`text-sm ${textClasses.muted} font-light`}>
For questions about these terms, contact us at{' '}
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
{settings.company_email}

View File

@@ -21,6 +21,18 @@ export interface PageContent {
email?: string;
address?: string;
};
contact_icons?: {
hero?: string;
email?: string;
phone?: string;
location?: string;
name_field?: string;
email_field?: string;
phone_field?: string;
subject_field?: string;
message_field?: string;
submit_button?: string;
};
map_url?: string;
social_links?: {
facebook?: string;
@@ -46,6 +58,13 @@ export interface PageContent {
features_section_title?: string;
features_section_subtitle?: string;
about_hero_image?: string;
about_hero_icon?: string;
about_contact_icons?: {
location?: string;
phone?: string;
email?: string;
};
about_learn_more_icon?: string;
mission?: string;
vision?: string;
team?: Array<{ name: string; role: string; image?: string; bio?: string; social_links?: { linkedin?: string; twitter?: string; email?: string } }>;
@@ -88,6 +107,7 @@ export interface PageContent {
services_section_button_text?: string;
services_section_button_link?: string;
services_section_limit?: number;
services_fallback_icon?: string;
luxury_services?: Array<{
icon?: string;
title: string;
@@ -204,6 +224,18 @@ export interface UpdatePageContentData {
email?: string;
address?: string;
};
contact_icons?: {
hero?: string;
email?: string;
phone?: string;
location?: string;
name_field?: string;
email_field?: string;
phone_field?: string;
subject_field?: string;
message_field?: string;
submit_button?: string;
};
map_url?: string;
social_links?: {
facebook?: string;
@@ -229,6 +261,13 @@ export interface UpdatePageContentData {
features_section_title?: string;
features_section_subtitle?: string;
about_hero_image?: string;
about_hero_icon?: string;
about_contact_icons?: {
location?: string;
phone?: string;
email?: string;
};
about_learn_more_icon?: string;
mission?: string;
vision?: string;
team?: Array<{ name: string; role: string; image?: string; bio?: string; social_links?: { linkedin?: string; twitter?: string; email?: string } }>;
@@ -271,6 +310,7 @@ export interface UpdatePageContentData {
services_section_button_text?: string;
services_section_button_link?: string;
services_section_limit?: number;
services_fallback_icon?: string;
luxury_services?: Array<{
icon?: string;
title: string;

View File

@@ -6,6 +6,7 @@ import useAuthStore from '../../../store/useAuthStore';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { toast } from 'react-toastify';
import ConfirmationDialog from '../../../shared/components/ConfirmationDialog';
import { formatWorkingHours } from '../../../shared/utils/format';
interface ChatWidgetProps {
onClose?: () => void;
@@ -405,7 +406,9 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
{!isWithinBusinessHours ? (
<p className="text-xs text-slate-700/80 font-light flex items-center gap-1">
<Clock className="w-3 h-3" />
Chat available 9 AM - 5 PM
Chat available {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined
? formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)
: '9:00 AM - 5:00 PM'}
</p>
) : chat?.status === 'pending' ? (
<p className="text-xs text-slate-700/80 font-light">Waiting for staff...</p>
@@ -459,7 +462,9 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
<div>
<p className="text-sm font-semibold text-slate-900 mb-1">Chat Hours</p>
<p className="text-xs text-slate-600 font-light">
Our chat support is available Monday to Friday, {settings.chat_working_hours_start || 9}:00 AM - {settings.chat_working_hours_end || 17}:00 PM.
Our chat support is available Monday to Friday, {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined
? formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)
: '9:00 AM - 5:00 PM'}.
Please leave your inquiry below and we'll get back to you as soon as possible.
</p>
</div>
@@ -585,6 +590,18 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
{}
{!showVisitorForm && isWithinBusinessHours && (
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-slate-50/50 to-white">
{/* Working Hours Info Banner */}
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
<div className="mb-4 p-3 bg-gradient-to-r from-[var(--luxury-gold)]/5 to-[var(--luxury-gold-dark)]/5 rounded-lg border border-[var(--luxury-gold)]/20">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-[var(--luxury-gold)] flex-shrink-0" />
<p className="text-xs text-slate-600 font-light">
<span className="font-medium text-slate-700">Chat Support Hours:</span>{' '}
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
</p>
</div>
</div>
)}
{loading && !chat ? (
<div className="text-center text-slate-500 py-8 font-light">
Starting chat...

View File

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

View File

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

View File

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

View File

@@ -44,6 +44,8 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
// Normalize value - ensure it's never an empty string, use a default if needed
const normalizedValue = value && value.trim() ? value : undefined;
const allIcons = useMemo(() => {
const icons: string[] = [];
@@ -59,25 +61,57 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
'memo'
]);
for (const iconName in LucideIcons) {
if (
excludedNames.has(iconName) ||
iconName.startsWith('_') ||
iconName[0] !== iconName[0].toUpperCase()
) {
continue;
try {
// Check if LucideIcons is available
if (!LucideIcons || typeof LucideIcons !== 'object') {
console.error('IconPicker: LucideIcons is not available');
// Return popular icons as fallback
return popularIcons.filter(icon => icon);
}
const iconComponent = (LucideIcons as any)[iconName];
for (const iconName in LucideIcons) {
// Skip if excluded
if (excludedNames.has(iconName)) {
continue;
}
if (typeof iconComponent === 'function') {
icons.push(iconName);
// Skip if starts with underscore
if (iconName.startsWith('_')) {
continue;
}
// Skip if empty or doesn't start with uppercase letter
if (!iconName || iconName.length === 0) {
continue;
}
// Check if first character is uppercase letter
const firstChar = iconName.charAt(0);
if (firstChar < 'A' || firstChar > 'Z') {
continue;
}
const iconComponent = (LucideIcons as any)[iconName];
// Only include if it's a function (React component)
if (typeof iconComponent === 'function') {
icons.push(iconName);
}
}
// If no icons found, use popular icons as fallback
if (icons.length === 0) {
console.warn('IconPicker: No icons found, using popular icons as fallback');
return popularIcons.filter(icon => icon);
}
const sorted = icons.sort();
return sorted;
} catch (error) {
console.error('IconPicker: Error loading icons:', error);
// Return popular icons as fallback
return popularIcons.filter(icon => icon);
}
const sorted = icons.sort();
return sorted;
}, []);
@@ -93,7 +127,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
);
}, [searchQuery, allIcons]);
const selectedIcon = value && (LucideIcons as any)[value] ? (LucideIcons as any)[value] : null;
const selectedIcon = normalizedValue && (LucideIcons as any)[normalizedValue] ? (LucideIcons as any)[normalizedValue] : null;
const handleIconSelect = (iconName: string) => {
onChange(iconName);
@@ -113,7 +147,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
{selectedIcon ? (
<>
{React.createElement(selectedIcon, { className: 'w-5 h-5 text-gray-700' })}
<span className="text-gray-700 font-medium">{value}</span>
<span className="text-gray-700 font-medium">{normalizedValue}</span>
</>
) : (
<span className="text-gray-400">Select an icon</span>
@@ -152,7 +186,12 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
</div>
</div>
<div className="overflow-y-auto p-4">
{filteredIcons.length > 0 ? (
{allIcons.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<p className="text-sm">Unable to load icons. Please refresh the page.</p>
<p className="text-xs mt-2">If the problem persists, check the browser console for errors.</p>
</div>
) : filteredIcons.length > 0 ? (
<>
{!searchQuery.trim() && (
<div className="mb-3">
@@ -166,7 +205,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
const IconComponent = (LucideIcons as any)[iconName];
if (!IconComponent) return null;
const isSelected = value === iconName;
const isSelected = normalizedValue === iconName;
const isPopular = !searchQuery.trim() && popularIcons.includes(iconName);
try {
@@ -208,8 +247,14 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
</>
) : (
<div className="text-center py-8 text-gray-500">
<p>No icons found matching "{searchQuery}"</p>
<p className="text-xs mt-2">Try a different search term</p>
{searchQuery.trim() ? (
<>
<p>No icons found matching "{searchQuery}"</p>
<p className="text-xs mt-2">Try a different search term</p>
</>
) : (
<p className="text-sm">No icons available</p>
)}
</div>
)}
</div>

View File

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

View File

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

View File

@@ -250,6 +250,7 @@ const PageContentDashboard: React.FC = () => {
services_section_button_text: contents.home.services_section_button_text || '',
services_section_button_link: contents.home.services_section_button_link || '',
services_section_limit: contents.home.services_section_limit || 6,
services_fallback_icon: contents.home.services_fallback_icon || 'Award',
luxury_experiences_section_title: contents.home.luxury_experiences_section_title || '',
luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '',
luxury_experiences: normalizeArray(contents.home.luxury_experiences),
@@ -284,19 +285,56 @@ const PageContentDashboard: React.FC = () => {
// Contact
if (contents.contact) {
const defaultContactIcons = {
hero: 'Mail',
email: 'Mail',
phone: 'Phone',
location: 'MapPin',
name_field: 'User',
email_field: 'Mail',
phone_field: 'Phone',
subject_field: 'MessageSquare',
message_field: 'MessageSquare',
submit_button: 'Send',
};
setContactData({
title: contents.contact.title || '',
subtitle: contents.contact.subtitle || '',
description: contents.contact.description || '',
content: contents.contact.content || '',
map_url: contents.contact.map_url || '',
contact_icons: contents.contact.contact_icons ? {
...defaultContactIcons,
...contents.contact.contact_icons,
} : defaultContactIcons,
meta_title: contents.contact.meta_title || '',
meta_description: contents.contact.meta_description || '',
});
} else {
// Initialize with defaults even if no contact data exists
setContactData({
contact_icons: {
hero: 'Mail',
email: 'Mail',
phone: 'Phone',
location: 'MapPin',
name_field: 'User',
email_field: 'Mail',
phone_field: 'Phone',
subject_field: 'MessageSquare',
message_field: 'MessageSquare',
submit_button: 'Send',
},
});
}
// About
if (contents.about) {
const defaultAboutContactIcons = {
location: 'MapPin',
phone: 'Phone',
email: 'Mail',
};
setAboutData({
title: contents.about.title || '',
subtitle: contents.about.subtitle || '',
@@ -306,6 +344,12 @@ const PageContentDashboard: React.FC = () => {
values: normalizeArray(contents.about.values),
features: normalizeArray(contents.about.features),
about_hero_image: contents.about.about_hero_image || '',
about_hero_icon: contents.about.about_hero_icon || 'Hotel',
about_contact_icons: contents.about.about_contact_icons ? {
...defaultAboutContactIcons,
...contents.about.about_contact_icons,
} : defaultAboutContactIcons,
about_learn_more_icon: contents.about.about_learn_more_icon || 'Hotel',
mission: contents.about.mission || '',
vision: contents.about.vision || '',
team: normalizeArray(contents.about.team),
@@ -314,6 +358,17 @@ const PageContentDashboard: React.FC = () => {
meta_title: contents.about.meta_title || '',
meta_description: contents.about.meta_description || '',
});
} else {
// Initialize with defaults even if no about data exists
setAboutData({
about_hero_icon: 'Hotel',
about_contact_icons: {
location: 'MapPin',
phone: 'Phone',
email: 'Mail',
},
about_learn_more_icon: 'Hotel',
});
}
// Footer
@@ -1354,7 +1409,7 @@ const PageContentDashboard: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<IconPicker
value={amenity?.icon || ''}
value={amenity?.icon && amenity.icon.trim() ? amenity.icon : 'Sparkles'}
onChange={(iconName) => {
setHomeData((prevData) => {
const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
@@ -1550,7 +1605,7 @@ const PageContentDashboard: React.FC = () => {
</div>
<div>
<IconPicker
value={feature?.icon || ''}
value={feature?.icon && feature.icon.trim() ? feature.icon : 'Star'}
onChange={(iconName) => {
setHomeData((prevData) => {
const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
@@ -2031,7 +2086,7 @@ const PageContentDashboard: React.FC = () => {
</div>
<div>
<IconPicker
value={stat?.icon || ''}
value={stat?.icon && stat.icon.trim() ? stat.icon : 'TrendingUp'}
onChange={(iconName) => {
const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
currentStats[index] = { ...currentStats[index], icon: iconName };
@@ -2177,7 +2232,7 @@ const PageContentDashboard: React.FC = () => {
</div>
<div>
<IconPicker
value={feature?.icon || ''}
value={feature?.icon && feature.icon.trim() ? feature.icon : 'Star'}
onChange={(iconName) => {
setHomeData((prevData) => {
const currentFeatures = Array.isArray(prevData.features) ? [...prevData.features] : [];
@@ -2303,6 +2358,14 @@ const PageContentDashboard: React.FC = () => {
placeholder="6"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Services Fallback Icon</label>
<p className="text-xs text-gray-500 mb-2">Icon shown when a service has no icon set</p>
<IconPicker
value={homeData.services_fallback_icon && homeData.services_fallback_icon.trim() ? homeData.services_fallback_icon : 'Award'}
onChange={(icon) => setHomeData({ ...homeData, services_fallback_icon: icon })}
/>
</div>
</div>
</div>
@@ -2371,7 +2434,7 @@ const PageContentDashboard: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<IconPicker
value={experience?.icon || ''}
value={experience?.icon && experience.icon.trim() ? experience.icon : 'Sparkles'}
onChange={(iconName) => {
const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
current[index] = { ...current[index], icon: iconName };
@@ -2522,7 +2585,7 @@ const PageContentDashboard: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<IconPicker
value={award?.icon || ''}
value={award?.icon && award.icon.trim() ? award.icon : 'Award'}
onChange={(iconName) => {
const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
current[index] = { ...current[index], icon: iconName };
@@ -3607,6 +3670,112 @@ const PageContentDashboard: React.FC = () => {
</div>
</div>
<div className="border-t border-gray-200 pt-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">Icons</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Hero Section Icon</label>
<IconPicker
value={contactData.contact_icons?.hero && contactData.contact_icons.hero.trim() ? contactData.contact_icons.hero : 'Mail'}
onChange={(icon) => setContactData({
...contactData,
contact_icons: { ...contactData.contact_icons, hero: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Email Contact Icon</label>
<IconPicker
value={contactData.contact_icons?.email && contactData.contact_icons.email.trim() ? contactData.contact_icons.email : 'Mail'}
onChange={(icon) => setContactData({
...contactData,
contact_icons: { ...contactData.contact_icons, email: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Phone Contact Icon</label>
<IconPicker
value={contactData.contact_icons?.phone && contactData.contact_icons.phone.trim() ? contactData.contact_icons.phone : 'Phone'}
onChange={(icon) => setContactData({
...contactData,
contact_icons: { ...contactData.contact_icons, phone: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Location Contact Icon</label>
<IconPicker
value={contactData.contact_icons?.location && contactData.contact_icons.location.trim() ? contactData.contact_icons.location : 'MapPin'}
onChange={(icon) => setContactData({
...contactData,
contact_icons: { ...contactData.contact_icons, location: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Name Field Icon</label>
<IconPicker
value={contactData.contact_icons?.name_field && contactData.contact_icons.name_field.trim() ? contactData.contact_icons.name_field : 'User'}
onChange={(icon) => setContactData({
...contactData,
contact_icons: { ...contactData.contact_icons, name_field: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Email Field Icon</label>
<IconPicker
value={contactData.contact_icons?.email_field && contactData.contact_icons.email_field.trim() ? contactData.contact_icons.email_field : 'Mail'}
onChange={(icon) => setContactData({
...contactData,
contact_icons: { ...contactData.contact_icons, email_field: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Phone Field Icon</label>
<IconPicker
value={contactData.contact_icons?.phone_field && contactData.contact_icons.phone_field.trim() ? contactData.contact_icons.phone_field : 'Phone'}
onChange={(icon) => setContactData({
...contactData,
contact_icons: { ...contactData.contact_icons, phone_field: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Subject Field Icon</label>
<IconPicker
value={contactData.contact_icons?.subject_field && contactData.contact_icons.subject_field.trim() ? contactData.contact_icons.subject_field : 'MessageSquare'}
onChange={(icon) => setContactData({
...contactData,
contact_icons: { ...contactData.contact_icons, subject_field: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Message Field Icon</label>
<IconPicker
value={contactData.contact_icons?.message_field && contactData.contact_icons.message_field.trim() ? contactData.contact_icons.message_field : 'MessageSquare'}
onChange={(icon) => setContactData({
...contactData,
contact_icons: { ...contactData.contact_icons, message_field: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Submit Button Icon</label>
<IconPicker
value={contactData.contact_icons?.submit_button && contactData.contact_icons.submit_button.trim() ? contactData.contact_icons.submit_button : 'Send'}
onChange={(icon) => setContactData({
...contactData,
contact_icons: { ...contactData.contact_icons, submit_button: icon }
})}
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
@@ -3727,6 +3896,15 @@ const PageContentDashboard: React.FC = () => {
)}
</div>
{/* Hero Icon (when no hero image) */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Hero Icon (shown when no hero image)</label>
<IconPicker
value={aboutData.about_hero_icon && aboutData.about_hero_icon.trim() ? aboutData.about_hero_icon : 'Hotel'}
onChange={(icon) => setAboutData({ ...aboutData, about_hero_icon: icon })}
/>
</div>
{/* Mission */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Mission Statement</label>
@@ -3792,7 +3970,7 @@ const PageContentDashboard: React.FC = () => {
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
<IconPicker
value={value.icon || 'Heart'}
value={value.icon && value.icon.trim() ? value.icon : 'Heart'}
onChange={(icon: string) => {
setAboutData((prevData) => {
const newValues = [...(prevData.values || [])];
@@ -3883,7 +4061,7 @@ const PageContentDashboard: React.FC = () => {
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
<IconPicker
value={feature.icon || 'Star'}
value={feature.icon && feature.icon.trim() ? feature.icon : 'Star'}
onChange={(icon: string) => {
setAboutData((prevData) => {
const newFeatures = [...(prevData.features || [])];
@@ -4262,7 +4440,7 @@ const PageContentDashboard: React.FC = () => {
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
<IconPicker
value={achievement.icon || 'Award'}
value={achievement.icon && achievement.icon.trim() ? achievement.icon : 'Award'}
onChange={(icon: string) => {
setAboutData((prevData) => {
const newAchievements = [...(prevData.achievements || [])];
@@ -4378,6 +4556,50 @@ const PageContentDashboard: React.FC = () => {
)}
</div>
{/* Additional Icons */}
<div className="border-t border-gray-200 pt-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">Additional Icons</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Contact Info - Location Icon</label>
<IconPicker
value={aboutData.about_contact_icons?.location && aboutData.about_contact_icons.location.trim() ? aboutData.about_contact_icons.location : 'MapPin'}
onChange={(icon) => setAboutData({
...aboutData,
about_contact_icons: { ...aboutData.about_contact_icons, location: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Contact Info - Phone Icon</label>
<IconPicker
value={aboutData.about_contact_icons?.phone && aboutData.about_contact_icons.phone.trim() ? aboutData.about_contact_icons.phone : 'Phone'}
onChange={(icon) => setAboutData({
...aboutData,
about_contact_icons: { ...aboutData.about_contact_icons, phone: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Contact Info - Email Icon</label>
<IconPicker
value={aboutData.about_contact_icons?.email && aboutData.about_contact_icons.email.trim() ? aboutData.about_contact_icons.email : 'Mail'}
onChange={(icon) => setAboutData({
...aboutData,
about_contact_icons: { ...aboutData.about_contact_icons, email: icon }
})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Learn More Button Icon</label>
<IconPicker
value={aboutData.about_learn_more_icon && aboutData.about_learn_more_icon.trim() ? aboutData.about_learn_more_icon : 'Hotel'}
onChange={(icon) => setAboutData({ ...aboutData, about_learn_more_icon: icon })}
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>

View File

@@ -134,6 +134,10 @@ const SettingsPage: React.FC = () => {
tax_rate: 0,
chat_working_hours_start: 9,
chat_working_hours_end: 17,
bank_name: '',
bank_account_number: '',
bank_account_holder: '',
bank_code: '',
});
const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [faviconPreview, setFaviconPreview] = useState<string | null>(null);
@@ -156,6 +160,7 @@ const SettingsPage: React.FC = () => {
theme_primary_light: '#f5d76e',
theme_primary_dark: '#c9a227',
theme_primary_accent: '#e8c547',
theme_layout_mode: 'dark',
});
const [loading, setLoading] = useState(true);
@@ -165,7 +170,6 @@ const SettingsPage: React.FC = () => {
const [openPaymentModal, setOpenPaymentModal] = useState<'stripe' | 'paypal' | 'borica' | null>(null);
const currencyNames: Record<string, string> = {
VND: 'Vietnamese Dong',
USD: 'US Dollar',
EUR: 'Euro',
GBP: 'British Pound',
@@ -300,6 +304,10 @@ const SettingsPage: React.FC = () => {
tax_rate: companyRes.data.tax_rate || 0,
chat_working_hours_start: companyRes.data.chat_working_hours_start || 9,
chat_working_hours_end: companyRes.data.chat_working_hours_end || 17,
bank_name: companyRes.data.bank_name || '',
bank_account_number: companyRes.data.bank_account_number || '',
bank_account_holder: companyRes.data.bank_account_holder || '',
bank_code: companyRes.data.bank_code || '',
});
@@ -804,6 +812,7 @@ const SettingsPage: React.FC = () => {
theme_primary_light: themeRes.data.theme_primary_light || '#f5d76e',
theme_primary_dark: themeRes.data.theme_primary_dark || '#c9a227',
theme_primary_accent: themeRes.data.theme_primary_accent || '#e8c547',
theme_layout_mode: themeRes.data.theme_layout_mode || 'dark',
});
} catch (error: any) {
toast.error(
@@ -2722,6 +2731,82 @@ const SettingsPage: React.FC = () => {
</p>
</div>
{/* Bank Details Section */}
<div className="space-y-4 pt-6 border-t border-gray-200">
<div className="flex items-center gap-2">
<Building2 className="w-5 h-5 text-[var(--luxury-gold)]" />
<h3 className="text-lg font-semibold text-gray-900">Bank Transfer Details</h3>
</div>
<p className="text-sm text-gray-600">
Configure bank details for bank transfer payments. These will be displayed on payment confirmation pages.
</p>
<div className="space-y-4">
<div>
<label className="block text-sm font-semibold text-gray-900 mb-2">
Bank Name
</label>
<input
type="text"
value={companyFormData.bank_name || ''}
onChange={(e) =>
setCompanyFormData({ ...companyFormData, bank_name: e.target.value })
}
placeholder="e.g., Bank of America, Chase Bank"
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-900 mb-2">
Account Number
</label>
<input
type="text"
value={companyFormData.bank_account_number || ''}
onChange={(e) =>
setCompanyFormData({ ...companyFormData, bank_account_number: e.target.value })
}
placeholder="Bank account number"
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-900 mb-2">
Account Holder Name
</label>
<input
type="text"
value={companyFormData.bank_account_holder || ''}
onChange={(e) =>
setCompanyFormData({ ...companyFormData, bank_account_holder: e.target.value })
}
placeholder="Name on the bank account"
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-900 mb-2">
Bank Code (Optional)
</label>
<input
type="text"
value={companyFormData.bank_code || ''}
onChange={(e) =>
setCompanyFormData({ ...companyFormData, bank_code: e.target.value })
}
placeholder="Bank routing/swift code (if needed for QR codes)"
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
/>
<p className="text-xs text-gray-500 mt-1">
Optional: Bank code used for QR code generation
</p>
</div>
</div>
</div>
{}
<div className="space-y-4">
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
@@ -3124,14 +3209,79 @@ const SettingsPage: React.FC = () => {
</div>
</div>
{/* Layout Mode Section */}
<div className="pt-6 border-t border-gray-200">
<div className="space-y-4">
<div>
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide mb-3">
<Palette className="w-4 h-4 text-gray-600" />
Page Layout Theme
</label>
<p className="text-xs text-gray-600 mb-4">
Choose between dark (black) or light (white) background theme for all frontend pages
</p>
<div className="grid grid-cols-2 gap-4">
<button
type="button"
onClick={() => setThemeFormData({ ...themeFormData, theme_layout_mode: 'dark' })}
className={`p-6 rounded-xl border-2 transition-all duration-200 ${
themeFormData.theme_layout_mode === 'dark'
? 'border-amber-500 bg-amber-50 shadow-lg'
: 'border-gray-200 bg-white hover:border-gray-300'
}`}
>
<div className="flex flex-col items-center gap-3">
<div className="w-16 h-16 rounded-lg bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] border-2 border-gray-300 shadow-inner"></div>
<div className="text-center">
<p className={`font-semibold text-sm ${themeFormData.theme_layout_mode === 'dark' ? 'text-amber-700' : 'text-gray-700'}`}>
Dark Theme
</p>
<p className="text-xs text-gray-500 mt-1">Black backgrounds</p>
</div>
{themeFormData.theme_layout_mode === 'dark' && (
<div className="w-5 h-5 rounded-full bg-amber-500 flex items-center justify-center">
<CheckCircle2 className="w-4 h-4 text-white" />
</div>
)}
</div>
</button>
<button
type="button"
onClick={() => setThemeFormData({ ...themeFormData, theme_layout_mode: 'light' })}
className={`p-6 rounded-xl border-2 transition-all duration-200 ${
themeFormData.theme_layout_mode === 'light'
? 'border-amber-500 bg-amber-50 shadow-lg'
: 'border-gray-200 bg-white hover:border-gray-300'
}`}
>
<div className="flex flex-col items-center gap-3">
<div className="w-16 h-16 rounded-lg bg-gradient-to-br from-gray-50 via-white to-gray-50 border-2 border-gray-300 shadow-inner"></div>
<div className="text-center">
<p className={`font-semibold text-sm ${themeFormData.theme_layout_mode === 'light' ? 'text-amber-700' : 'text-gray-700'}`}>
Light Theme
</p>
<p className="text-xs text-gray-500 mt-1">White backgrounds</p>
</div>
{themeFormData.theme_layout_mode === 'light' && (
<div className="w-5 h-5 rounded-full bg-amber-500 flex items-center justify-center">
<CheckCircle2 className="w-4 h-4 text-white" />
</div>
)}
</div>
</button>
</div>
</div>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-800">
<p className="font-semibold mb-1">About Theme Colors</p>
<p className="font-semibold mb-1">About Theme Settings</p>
<p className="text-xs">
Changes to theme colors will be applied immediately across all frontend pages.
Use hex color codes (e.g., #d4af37) for best results. The colors will replace the default gold/yellow theme throughout the application.
Changes to theme colors and layout mode will be applied immediately across all frontend pages.
Use hex color codes (e.g., #d4af37) for best results. The layout mode controls whether pages use dark (black) or light (white) backgrounds.
</p>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

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 { logger } from '../../shared/utils/logger';
import { toast } from 'react-toastify';
import { useTheme } from '../../shared/contexts/ThemeContext';
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../shared/utils/themeUtils';
const RoomListPage: React.FC = () => {
const { theme } = useTheme();
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
const [searchParams] = useSearchParams();
const [rooms, setRooms] = useState<Room[]>([]);
const [loading, setLoading] = useState(true);
@@ -146,13 +153,13 @@ const RoomListPage: React.FC = () => {
}, [searchParams]);
return (
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
{}
{/* Promotion Banner */}
{showPromotionBanner && activePromotion && (
<div className="w-full bg-gradient-to-r from-[var(--luxury-gold)]/20 via-[var(--luxury-gold-light)]/15 to-[var(--luxury-gold)]/20 border-b border-[var(--luxury-gold)]/30">
<div className="w-full max-w-[1920px] mx-auto px-3 sm:px-4 md:px-6 lg:px-8 xl:px-12 2xl:px-16 3xl:px-20 py-4">
<div className="flex items-center justify-between gap-4 bg-gradient-to-r from-[#1a1a1a] to-[#0f0f0f] border border-[var(--luxury-gold)]/40 rounded-lg p-4 backdrop-blur-xl shadow-lg">
<div className={`flex items-center justify-between gap-4 ${cardClasses} border border-[var(--luxury-gold)]/40 rounded-lg p-4 backdrop-blur-xl shadow-lg`}>
<div className="flex items-center gap-3 flex-1">
<div className="p-2 bg-[var(--luxury-gold)]/20 rounded-lg border border-[var(--luxury-gold)]/40">
<Tag className="w-5 h-5 text-[var(--luxury-gold)]" />
@@ -165,11 +172,11 @@ const RoomListPage: React.FC = () => {
</span>
</div>
{activePromotion.discount && (
<p className="text-xs text-gray-300">
<p className={`text-xs ${textClasses.secondary}`}>
{activePromotion.discount} - {activePromotion.description || 'Valid on bookings'}
</p>
)}
<p className="text-xs text-gray-400 mt-1">
<p className={`text-xs ${textClasses.muted} mt-1`}>
The promotion code will be automatically applied when you book a room
</p>
</div>
@@ -179,14 +186,14 @@ const RoomListPage: React.FC = () => {
className="p-2 hover:bg-[var(--luxury-gold)]/10 rounded-lg transition-colors"
aria-label="Dismiss promotion"
>
<X className="w-5 h-5 text-gray-400 hover:text-white" />
<X className={`w-5 h-5 ${textClasses.muted} ${theme.theme_layout_mode === 'light' ? 'hover:text-gray-900' : 'hover:text-white'}`} />
</button>
</div>
</div>
</div>
)}
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8">
<div className={`w-full ${heroBgClasses} border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8`}>
<div className="w-full max-w-[1920px] mx-auto px-3 sm:px-4 md:px-6 lg:px-8 xl:px-12 2xl:px-16 3xl:px-20 py-6 sm:py-7 md:py-8 lg:py-10">
{}
<Link
@@ -212,14 +219,16 @@ const RoomListPage: React.FC = () => {
<Hotel className="w-5 h-5 sm:w-5 sm:h-5 text-[var(--luxury-gold)]" />
</div>
</div>
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold
text-white mb-2 sm:mb-3 tracking-tight leading-tight px-2
bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white
bg-clip-text text-transparent"
<h1 className={`text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold
${textClasses.primary} mb-2 sm:mb-3 tracking-tight leading-tight px-2
${theme.theme_layout_mode === 'light'
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'}
bg-clip-text text-transparent`}
>
Our Rooms & Suites
</h1>
<p className="text-gray-400 font-light tracking-wide text-xs sm:text-sm md:text-base max-w-xl mx-auto px-2 sm:px-4 leading-relaxed">
<p className={`${textClasses.muted} font-light tracking-wide text-xs sm:text-sm md:text-base max-w-xl mx-auto px-2 sm:px-4 leading-relaxed`}>
Discover our collection of luxurious accommodations,
each designed to provide an exceptional stay
</p>
@@ -235,18 +244,18 @@ const RoomListPage: React.FC = () => {
<div className="xl:hidden order-1 mb-4">
<button
onClick={() => setIsFilterOpen(!isFilterOpen)}
className="w-full bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
className={`w-full ${cardClasses}
border border-[var(--luxury-gold)]/30 rounded-xl p-4
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/10
flex items-center justify-between gap-3
hover:border-[var(--luxury-gold)]/50 hover:shadow-xl hover:shadow-[var(--luxury-gold)]/20
transition-all duration-300 touch-manipulation"
transition-all duration-300 touch-manipulation`}
>
<div className="flex items-center gap-3">
<div className="p-2 bg-[var(--luxury-gold)]/10 rounded-lg border border-[var(--luxury-gold)]/30">
<Filter className="w-5 h-5 text-[var(--luxury-gold)]" />
</div>
<span className="text-white font-medium tracking-wide text-base">
<span className={`${textClasses.primary} font-medium tracking-wide text-base`}>
Filters
</span>
</div>
@@ -320,21 +329,21 @@ const RoomListPage: React.FC = () => {
)}
{!loading && !error && rooms.length === 0 && (
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
<div className={`${cardClasses}
border border-[var(--luxury-gold)]/20 rounded-xl p-8 sm:p-10 md:p-12 lg:p-16 text-center
backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5"
backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5`}
>
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24
bg-[var(--luxury-gold)]/10 rounded-2xl mb-4 sm:mb-5 md:mb-6 lg:mb-8 border border-[var(--luxury-gold)]/30"
>
<Hotel className="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 text-[var(--luxury-gold)]" />
</div>
<h3 className="text-lg sm:text-xl md:text-2xl font-serif font-semibold
text-white mb-3 sm:mb-4 tracking-wide px-2"
<h3 className={`text-lg sm:text-xl md:text-2xl font-serif font-semibold
${textClasses.primary} mb-3 sm:mb-4 tracking-wide px-2`}
>
No matching rooms found
</h3>
<p className="text-gray-400 font-light tracking-wide mb-5 sm:mb-6 md:mb-8 text-sm sm:text-base md:text-lg px-2">
<p className={`${textClasses.muted} font-light tracking-wide mb-5 sm:mb-6 md:mb-8 text-sm sm:text-base md:text-lg px-2`}>
Please try adjusting the filters or search differently
</p>
<button
@@ -354,7 +363,7 @@ const RoomListPage: React.FC = () => {
<>
{}
<div className="mb-3 sm:mb-4 md:mb-5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-3">
<p className="text-gray-400 font-light tracking-wide text-xs sm:text-sm md:text-base">
<p className={`${textClasses.muted} font-light tracking-wide text-xs sm:text-sm md:text-base`}>
Showing <span className="text-[var(--luxury-gold)] font-medium">{rooms.length}</span> of{' '}
<span className="text-[var(--luxury-gold)] font-medium">{pagination.total}</span> rooms
</p>

View File

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

View File

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

View File

@@ -29,11 +29,18 @@ import CookiePreferencesLink from './CookiePreferencesLink';
import ChatWidget from '../../features/notifications/components/ChatWidget';
import pageContentService, { type PageContent } from '../../features/content/services/pageContentService';
import { useCompanySettings } from '../contexts/CompanySettingsContext';
import { useTheme } from '../contexts/ThemeContext';
import apiClient from '../services/apiClient';
import { formatWorkingHours } from '../utils/format';
import { Clock } from 'lucide-react';
import { getThemeTextClasses, getThemeCardClasses } from '../utils/themeUtils';
const Footer: React.FC = () => {
const { settings } = useCompanySettings();
const { theme } = useTheme();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
const [homePageContent, setHomePageContent] = useState<PageContent | null>(null);
const [enabledPages, setEnabledPages] = useState<Set<string>>(new Set());
const [apiError, setApiError] = useState(false);
@@ -192,7 +199,9 @@ const Footer: React.FC = () => {
});
return (
<footer className="relative bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black text-gray-300 overflow-hidden">
<footer className={`relative ${theme.theme_layout_mode === 'light'
? 'bg-gradient-to-b from-gray-50 via-white to-gray-100'
: 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black'} ${textClasses.secondary} overflow-hidden`}>
{/* Top Gold Accent Line */}
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent shadow-lg shadow-[var(--luxury-gold)]/50"></div>
@@ -228,7 +237,7 @@ const Footer: React.FC = () => {
</div>
)}
<div>
<h2 className="text-2xl sm:text-3xl font-display font-semibold text-white tracking-tight mb-1">
<h2 className={`text-2xl sm:text-3xl font-display font-semibold ${textClasses.primary} tracking-tight mb-1`}>
{settings.company_name || pageContent?.title || 'Luxury Hotel'}
</h2>
<p className="text-xs sm:text-sm text-[var(--luxury-gold)] font-light tracking-[3px] sm:tracking-[4px] uppercase">
@@ -236,7 +245,7 @@ const Footer: React.FC = () => {
</p>
</div>
</div>
<p className="text-sm sm:text-base text-gray-400 mb-8 leading-relaxed max-w-md font-light">
<p className={`text-sm sm:text-base ${textClasses.muted} mb-8 leading-relaxed max-w-md font-light`}>
{pageContent?.description || 'Experience unparalleled luxury and world-class hospitality. Your journey to exceptional comfort begins here.'}
</p>
@@ -252,7 +261,7 @@ const Footer: React.FC = () => {
className="group flex items-center space-x-2 px-3 py-2 bg-gradient-to-r from-[var(--luxury-gold)]/5 to-transparent border border-[var(--luxury-gold)]/10 rounded-lg hover:border-[var(--luxury-gold)]/30 hover:from-[var(--luxury-gold)]/10 transition-all duration-300"
>
<BadgeIcon className="w-4 h-4 sm:w-5 sm:h-5 text-[var(--luxury-gold)] group-hover:scale-110 transition-transform duration-300" />
<span className="text-xs sm:text-sm font-medium tracking-wide text-gray-300 group-hover:text-[var(--luxury-gold)] transition-colors">{badge.text}</span>
<span className={`text-xs sm:text-sm font-medium tracking-wide ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-colors`}>{badge.text}</span>
</div>
);
})}
@@ -266,10 +275,12 @@ const Footer: React.FC = () => {
href={pageContent.social_links.facebook}
target="_blank"
rel="noopener noreferrer"
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
? 'bg-white border border-gray-300 shadow-sm'
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
aria-label="Facebook"
>
<Facebook className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
<Facebook className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
</a>
)}
@@ -278,10 +289,12 @@ const Footer: React.FC = () => {
href={pageContent.social_links.twitter}
target="_blank"
rel="noopener noreferrer"
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
? 'bg-white border border-gray-300 shadow-sm'
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
aria-label="Twitter"
>
<Twitter className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
<Twitter className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
</a>
)}
@@ -290,10 +303,12 @@ const Footer: React.FC = () => {
href={pageContent.social_links.instagram}
target="_blank"
rel="noopener noreferrer"
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
? 'bg-white border border-gray-300 shadow-sm'
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
aria-label="Instagram"
>
<Instagram className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
<Instagram className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
</a>
)}
@@ -302,10 +317,12 @@ const Footer: React.FC = () => {
href={pageContent.social_links.linkedin}
target="_blank"
rel="noopener noreferrer"
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
? 'bg-white border border-gray-300 shadow-sm'
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
aria-label="LinkedIn"
>
<Linkedin className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
<Linkedin className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
</a>
)}
@@ -314,10 +331,12 @@ const Footer: React.FC = () => {
href={pageContent.social_links.youtube}
target="_blank"
rel="noopener noreferrer"
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
? 'bg-white border border-gray-300 shadow-sm'
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
aria-label="YouTube"
>
<Youtube className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
<Youtube className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
</a>
)}
@@ -327,7 +346,7 @@ const Footer: React.FC = () => {
{/* Quick Links */}
{quickLinks.length > 0 && (
<div className="lg:col-span-2">
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide">
<h3 className={`${textClasses.primary} font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide`}>
<span className="relative z-10">Quick Links</span>
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
</h3>
@@ -336,7 +355,7 @@ const Footer: React.FC = () => {
<li key={link.url}>
<Link
to={link.url}
className="group flex items-center text-sm sm:text-base text-gray-400 hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide"
className={`group flex items-center text-sm sm:text-base ${textClasses.muted} hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide`}
>
<span className="absolute left-0 w-0 h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] group-hover:w-8 transition-all duration-300 rounded-full"></span>
<span className="ml-10 group-hover:translate-x-2 transition-transform duration-300 group-hover:font-medium">{link.label}</span>
@@ -350,7 +369,7 @@ const Footer: React.FC = () => {
{/* Guest Services */}
{supportLinks.length > 0 && (
<div className="lg:col-span-2">
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide">
<h3 className={`${textClasses.primary} font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide`}>
<span className="relative z-10">Guest Services</span>
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
</h3>
@@ -359,7 +378,7 @@ const Footer: React.FC = () => {
<li key={link.url}>
<Link
to={link.url}
className="group flex items-center text-sm sm:text-base text-gray-400 hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide"
className={`group flex items-center text-sm sm:text-base ${textClasses.muted} hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide`}
>
<span className="absolute left-0 w-0 h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] group-hover:w-8 transition-all duration-300 rounded-full"></span>
<span className="ml-10 group-hover:translate-x-2 transition-transform duration-300 group-hover:font-medium">{link.label}</span>
@@ -377,7 +396,7 @@ const Footer: React.FC = () => {
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
</h3>
{homePageContent?.newsletter_section_subtitle && (
<p className="text-sm text-gray-400 mb-4 font-light leading-relaxed">
<p className={`text-sm ${textClasses.muted} mb-4 font-light leading-relaxed`}>
{homePageContent.newsletter_section_subtitle}
</p>
)}
@@ -416,7 +435,9 @@ const Footer: React.FC = () => {
value={newsletterEmail}
onChange={(e) => setNewsletterEmail(e.target.value)}
placeholder={homePageContent?.newsletter_placeholder || 'Enter your email'}
className="w-full px-4 py-2.5 rounded-lg border border-gray-700 bg-gray-800/50 text-white placeholder-gray-400 focus:border-[var(--luxury-gold)] focus:ring-2 focus:ring-[var(--luxury-gold)]/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm"
className={`w-full px-4 py-2.5 rounded-lg border ${theme.theme_layout_mode === 'light'
? 'border-gray-300 bg-white text-gray-900 placeholder-gray-500'
: 'border-gray-700 bg-gray-800/50 text-white placeholder-gray-400'} focus:border-[var(--luxury-gold)] focus:ring-2 focus:ring-[var(--luxury-gold)]/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm`}
required
disabled={newsletterSubmitting}
/>
@@ -441,7 +462,7 @@ const Footer: React.FC = () => {
{/* Contact Information */}
<div className="lg:col-span-2">
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide">
<h3 className={`${textClasses.primary} font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide`}>
<span className="relative z-10">Contact</span>
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
</h3>
@@ -455,7 +476,7 @@ const Footer: React.FC = () => {
</div>
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
</div>
<span className="text-sm sm:text-base text-gray-400 group-hover:text-gray-300 transition-colors leading-relaxed font-light pt-1">
<span className={`text-sm sm:text-base ${textClasses.muted} group-hover:${textClasses.secondary} transition-colors leading-relaxed font-light pt-1`}>
{displayAddress
.split('\n').map((line, i) => (
<React.Fragment key={i}>
@@ -474,7 +495,7 @@ const Footer: React.FC = () => {
</div>
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
</div>
<a href={phoneHref} className="text-sm sm:text-base text-gray-400 group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide">
<a href={phoneHref} className={`text-sm sm:text-base ${textClasses.muted} group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide`}>
{displayPhone}
</a>
</li>
@@ -487,11 +508,29 @@ const Footer: React.FC = () => {
</div>
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
</div>
<a href={`mailto:${displayEmail}`} className="text-sm sm:text-base text-gray-400 group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide break-all">
<a href={`mailto:${displayEmail}`} className={`text-sm sm:text-base ${textClasses.muted} group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide break-all`}>
{displayEmail}
</a>
</li>
)}
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
<li className="flex items-start space-x-4 group">
<div className="relative mt-1 flex-shrink-0">
<div className="p-2 bg-gradient-to-br from-[var(--luxury-gold)]/10 to-[var(--luxury-gold-dark)]/5 rounded-lg border border-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/40 transition-all duration-300">
<Clock className="w-4 h-4 sm:w-5 sm:h-5 text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
</div>
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
</div>
<div className="pt-1">
<span className={`text-sm sm:text-base ${textClasses.muted} group-hover:${textClasses.secondary} transition-colors font-light tracking-wide block`}>
Chat Support Hours
</span>
<span className="text-xs sm:text-sm text-gray-500 font-light">
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
</span>
</div>
</li>
)}
</ul>
)}

View File

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

View File

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

View File

@@ -17,6 +17,10 @@ type CompanySettings = {
company_address: string;
chat_working_hours_start: number;
chat_working_hours_end: number;
bank_name?: string;
bank_account_number?: string;
bank_account_holder?: string;
bank_code?: string;
};
type CompanySettingsContextValue = {
@@ -35,6 +39,10 @@ const defaultSettings: CompanySettings = {
company_address: '',
chat_working_hours_start: 9,
chat_working_hours_end: 17,
bank_name: '',
bank_account_number: '',
bank_account_holder: '',
bank_code: '',
};
const CompanySettingsContext = createContext<CompanySettingsContextValue | undefined>(undefined);
@@ -71,6 +79,10 @@ export const CompanySettingsProvider: React.FC<CompanySettingsProviderProps> = (
company_address: response.data.company_address || defaultSettings.company_address,
chat_working_hours_start: response.data.chat_working_hours_start || defaultSettings.chat_working_hours_start,
chat_working_hours_end: response.data.chat_working_hours_end || defaultSettings.chat_working_hours_end,
bank_name: response.data.bank_name || defaultSettings.bank_name,
bank_account_number: response.data.bank_account_number || defaultSettings.bank_account_number,
bank_account_holder: response.data.bank_account_holder || defaultSettings.bank_account_holder,
bank_code: response.data.bank_code || defaultSettings.bank_code,
});

View File

@@ -7,25 +7,28 @@ import React, {
} from 'react';
import { themeService } from '../../features/system/services/systemSettingsService';
type ThemeColors = {
primary: string;
primaryLight: string;
primaryDark: string;
primaryAccent: string;
type ThemeLayoutMode = 'dark' | 'light';
type ThemeSettings = {
theme_primary_color: string;
theme_primary_light: string;
theme_primary_dark: string;
theme_primary_accent: string;
theme_layout_mode: ThemeLayoutMode;
};
type ThemeContextValue = {
colors: ThemeColors;
theme: ThemeSettings;
isLoading: boolean;
refreshTheme: () => Promise<void>;
updateColors: (colors: Partial<ThemeColors>) => void;
};
const defaultColors: ThemeColors = {
primary: '#d4af37',
primaryLight: '#f5d76e',
primaryDark: '#c9a227',
primaryAccent: '#e8c547',
const defaultTheme: ThemeSettings = {
theme_primary_color: '#d4af37',
theme_primary_light: '#f5d76e',
theme_primary_dark: '#c9a227',
theme_primary_accent: '#e8c547',
theme_layout_mode: 'dark',
};
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
@@ -42,58 +45,8 @@ interface ThemeProviderProps {
children: ReactNode;
}
/**
* Converts hex color to RGB values
*/
const hexToRgb = (hex: string): string => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`
: '212, 175, 55'; // Default gold RGB
};
/**
* Applies theme colors as CSS variables to the document root
*/
const applyThemeColors = (colors: ThemeColors) => {
const root = document.documentElement;
// Apply CSS variables
root.style.setProperty('--luxury-gold', colors.primary);
root.style.setProperty('--luxury-gold-light', colors.primaryLight);
root.style.setProperty('--luxury-gold-dark', colors.primaryDark);
root.style.setProperty('--luxury-gold-accent', colors.primaryAccent);
// Add RGB versions for rgba() usage
root.style.setProperty('--luxury-gold-rgb', hexToRgb(colors.primary));
root.style.setProperty('--luxury-gold-light-rgb', hexToRgb(colors.primaryLight));
root.style.setProperty('--luxury-gold-dark-rgb', hexToRgb(colors.primaryDark));
root.style.setProperty('--luxury-gold-accent-rgb', hexToRgb(colors.primaryAccent));
// Also update gradient variables
root.style.setProperty(
'--gradient-gold',
`linear-gradient(135deg, ${colors.primary} 0%, ${colors.primaryLight} 100%)`
);
root.style.setProperty(
'--gradient-gold-dark',
`linear-gradient(135deg, ${colors.primaryDark} 0%, ${colors.primary} 100%)`
);
// Update shadow variables with proper opacity
const primaryRgb = hexToRgb(colors.primary);
root.style.setProperty(
'--shadow-luxury',
`0 4px 20px rgba(${primaryRgb}, 0.15)`
);
root.style.setProperty(
'--shadow-luxury-gold',
`0 8px 30px rgba(${primaryRgb}, 0.25)`
);
};
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const [colors, setColors] = useState<ThemeColors>(defaultColors);
const [theme, setTheme] = useState<ThemeSettings>(defaultTheme);
const [isLoading, setIsLoading] = useState<boolean>(true);
const loadTheme = async () => {
@@ -101,20 +54,34 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
setIsLoading(true);
const response = await themeService.getThemeSettings();
if (response.data) {
const newColors: ThemeColors = {
primary: response.data.theme_primary_color || defaultColors.primary,
primaryLight: response.data.theme_primary_light || defaultColors.primaryLight,
primaryDark: response.data.theme_primary_dark || defaultColors.primaryDark,
primaryAccent: response.data.theme_primary_accent || defaultColors.primaryAccent,
const themeData: ThemeSettings = {
theme_primary_color: response.data.theme_primary_color || defaultTheme.theme_primary_color,
theme_primary_light: response.data.theme_primary_light || defaultTheme.theme_primary_light,
theme_primary_dark: response.data.theme_primary_dark || defaultTheme.theme_primary_dark,
theme_primary_accent: response.data.theme_primary_accent || defaultTheme.theme_primary_accent,
theme_layout_mode: (response.data.theme_layout_mode === 'light' ? 'light' : 'dark') as ThemeLayoutMode,
};
setColors(newColors);
applyThemeColors(newColors);
setTheme(themeData);
// Apply CSS variables for colors
document.documentElement.style.setProperty('--luxury-gold', themeData.theme_primary_color);
document.documentElement.style.setProperty('--luxury-gold-light', themeData.theme_primary_light);
document.documentElement.style.setProperty('--luxury-gold-dark', themeData.theme_primary_dark);
document.documentElement.style.setProperty('--luxury-gold-accent', themeData.theme_primary_accent);
// Apply layout mode class to body
document.body.classList.remove('theme-dark', 'theme-light');
document.body.classList.add(`theme-${themeData.theme_layout_mode}`);
document.documentElement.classList.remove('theme-dark', 'theme-light');
document.documentElement.classList.add(`theme-${themeData.theme_layout_mode}`);
}
} catch (error) {
console.error('Error loading theme settings:', error);
// Apply default colors on error
applyThemeColors(defaultColors);
setColors(defaultColors);
// Apply defaults
document.body.classList.remove('theme-dark', 'theme-light');
document.body.classList.add('theme-dark');
document.documentElement.classList.remove('theme-dark', 'theme-light');
document.documentElement.classList.add('theme-dark');
} finally {
setIsLoading(false);
}
@@ -140,23 +107,15 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
await loadTheme();
};
const updateColors = (newColors: Partial<ThemeColors>) => {
const updatedColors = { ...colors, ...newColors };
setColors(updatedColors);
applyThemeColors(updatedColors);
};
return (
<ThemeContext.Provider
value={{
colors,
theme,
isLoading,
refreshTheme,
updateColors,
}}
>
{children}
</ThemeContext.Provider>
);
};

View File

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

View File

@@ -43,9 +43,7 @@ export const formatCurrency = (
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
if (isNaN(numAmount)) {
if (currency === 'VND') return '0 ₫';
return `0 ${currency || 'VND'}`;
return `0 ${currency || 'USD'}`;
}
@@ -67,16 +65,7 @@ export const formatCurrency = (
}
}
currencyToUse = currencyToUse || 'VND';
if (currencyToUse === 'VND') {
return new Intl.NumberFormat('vi-VN', {
style: 'currency',
currency: 'VND',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(numAmount);
}
currencyToUse = currencyToUse || 'USD';
const localeMap: Record<string, string> = {
@@ -242,3 +231,20 @@ export const truncateText = (
return text.slice(0, maxLength - suffix.length) + suffix;
};
/**
* Format working hours from 24-hour format to 12-hour format
* @param startHour - Start hour in 24-hour format (0-23)
* @param endHour - End hour in 24-hour format (0-23)
* @returns Formatted string like "9:00 AM - 5:00 PM"
*/
export const formatWorkingHours = (startHour: number, endHour: number): string => {
const formatHour = (hour: number): string => {
if (hour === 0) return '12:00 AM';
if (hour < 12) return `${hour}:00 AM`;
if (hour === 12) return '12:00 PM';
return `${hour - 12}:00 PM`;
};
return `${formatHour(startHour)} - ${formatHour(endHour)}`;
};

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