updates
This commit is contained in:
Binary file not shown.
@@ -1435,6 +1435,10 @@ class UpdateCompanySettingsRequest(BaseModel):
|
|||||||
tax_rate: Optional[float] = None
|
tax_rate: Optional[float] = None
|
||||||
chat_working_hours_start: Optional[int] = None
|
chat_working_hours_start: Optional[int] = None
|
||||||
chat_working_hours_end: Optional[int] = None
|
chat_working_hours_end: Optional[int] = None
|
||||||
|
bank_name: Optional[str] = None
|
||||||
|
bank_account_number: Optional[str] = None
|
||||||
|
bank_account_holder: Optional[str] = None
|
||||||
|
bank_code: Optional[str] = None
|
||||||
|
|
||||||
@router.get("/company")
|
@router.get("/company")
|
||||||
async def get_company_settings(
|
async def get_company_settings(
|
||||||
@@ -1452,6 +1456,10 @@ async def get_company_settings(
|
|||||||
"tax_rate",
|
"tax_rate",
|
||||||
"chat_working_hours_start",
|
"chat_working_hours_start",
|
||||||
"chat_working_hours_end",
|
"chat_working_hours_end",
|
||||||
|
"bank_name",
|
||||||
|
"bank_account_number",
|
||||||
|
"bank_account_holder",
|
||||||
|
"bank_code",
|
||||||
]
|
]
|
||||||
|
|
||||||
settings_dict = {}
|
settings_dict = {}
|
||||||
@@ -1488,6 +1496,10 @@ async def get_company_settings(
|
|||||||
"tax_rate": float(settings_dict.get("tax_rate", 0)) if settings_dict.get("tax_rate") else 0.0,
|
"tax_rate": float(settings_dict.get("tax_rate", 0)) if settings_dict.get("tax_rate") else 0.0,
|
||||||
"chat_working_hours_start": int(settings_dict.get("chat_working_hours_start", 9)) if settings_dict.get("chat_working_hours_start") else 9,
|
"chat_working_hours_start": int(settings_dict.get("chat_working_hours_start", 9)) if settings_dict.get("chat_working_hours_start") else 9,
|
||||||
"chat_working_hours_end": int(settings_dict.get("chat_working_hours_end", 17)) if settings_dict.get("chat_working_hours_end") else 17,
|
"chat_working_hours_end": int(settings_dict.get("chat_working_hours_end", 17)) if settings_dict.get("chat_working_hours_end") else 17,
|
||||||
|
"bank_name": settings_dict.get("bank_name", ""),
|
||||||
|
"bank_account_number": settings_dict.get("bank_account_number", ""),
|
||||||
|
"bank_account_holder": settings_dict.get("bank_account_holder", ""),
|
||||||
|
"bank_code": settings_dict.get("bank_code", ""),
|
||||||
"updated_at": updated_at,
|
"updated_at": updated_at,
|
||||||
"updated_by": updated_by,
|
"updated_by": updated_by,
|
||||||
}
|
}
|
||||||
@@ -1533,6 +1545,14 @@ async def update_company_settings(
|
|||||||
db_settings["chat_working_hours_start"] = str(request_data.chat_working_hours_start)
|
db_settings["chat_working_hours_start"] = str(request_data.chat_working_hours_start)
|
||||||
if request_data.chat_working_hours_end is not None:
|
if request_data.chat_working_hours_end is not None:
|
||||||
db_settings["chat_working_hours_end"] = str(request_data.chat_working_hours_end)
|
db_settings["chat_working_hours_end"] = str(request_data.chat_working_hours_end)
|
||||||
|
if request_data.bank_name is not None:
|
||||||
|
db_settings["bank_name"] = request_data.bank_name
|
||||||
|
if request_data.bank_account_number is not None:
|
||||||
|
db_settings["bank_account_number"] = request_data.bank_account_number
|
||||||
|
if request_data.bank_account_holder is not None:
|
||||||
|
db_settings["bank_account_holder"] = request_data.bank_account_holder
|
||||||
|
if request_data.bank_code is not None:
|
||||||
|
db_settings["bank_code"] = request_data.bank_code
|
||||||
|
|
||||||
for key, value in db_settings.items():
|
for key, value in db_settings.items():
|
||||||
|
|
||||||
@@ -1581,7 +1601,7 @@ async def update_company_settings(
|
|||||||
|
|
||||||
|
|
||||||
updated_settings = {}
|
updated_settings = {}
|
||||||
for key in ["company_name", "company_tagline", "company_logo_url", "company_favicon_url", "company_phone", "company_email", "company_address", "tax_rate", "chat_working_hours_start", "chat_working_hours_end"]:
|
for key in ["company_name", "company_tagline", "company_logo_url", "company_favicon_url", "company_phone", "company_email", "company_address", "tax_rate", "chat_working_hours_start", "chat_working_hours_end", "bank_name", "bank_account_number", "bank_account_holder", "bank_code"]:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == key
|
SystemSettings.key == key
|
||||||
).first()
|
).first()
|
||||||
@@ -1619,6 +1639,10 @@ async def update_company_settings(
|
|||||||
"tax_rate": float(updated_settings.get("tax_rate", 0)) if updated_settings.get("tax_rate") else 0.0,
|
"tax_rate": float(updated_settings.get("tax_rate", 0)) if updated_settings.get("tax_rate") else 0.0,
|
||||||
"chat_working_hours_start": int(updated_settings.get("chat_working_hours_start", 9)) if updated_settings.get("chat_working_hours_start") else 9,
|
"chat_working_hours_start": int(updated_settings.get("chat_working_hours_start", 9)) if updated_settings.get("chat_working_hours_start") else 9,
|
||||||
"chat_working_hours_end": int(updated_settings.get("chat_working_hours_end", 17)) if updated_settings.get("chat_working_hours_end") else 17,
|
"chat_working_hours_end": int(updated_settings.get("chat_working_hours_end", 17)) if updated_settings.get("chat_working_hours_end") else 17,
|
||||||
|
"bank_name": updated_settings.get("bank_name", ""),
|
||||||
|
"bank_account_number": updated_settings.get("bank_account_number", ""),
|
||||||
|
"bank_account_holder": updated_settings.get("bank_account_holder", ""),
|
||||||
|
"bank_code": updated_settings.get("bank_code", ""),
|
||||||
"updated_at": updated_at,
|
"updated_at": updated_at,
|
||||||
"updated_by": updated_by,
|
"updated_by": updated_by,
|
||||||
}
|
}
|
||||||
@@ -1851,6 +1875,7 @@ class UpdateThemeSettingsRequest(BaseModel):
|
|||||||
theme_primary_light: Optional[str] = None
|
theme_primary_light: Optional[str] = None
|
||||||
theme_primary_dark: Optional[str] = None
|
theme_primary_dark: Optional[str] = None
|
||||||
theme_primary_accent: Optional[str] = None
|
theme_primary_accent: Optional[str] = None
|
||||||
|
theme_layout_mode: Optional[str] = None # 'dark' or 'light'
|
||||||
|
|
||||||
@router.get("/theme")
|
@router.get("/theme")
|
||||||
async def get_theme_settings(
|
async def get_theme_settings(
|
||||||
@@ -1863,6 +1888,7 @@ async def get_theme_settings(
|
|||||||
"theme_primary_light",
|
"theme_primary_light",
|
||||||
"theme_primary_dark",
|
"theme_primary_dark",
|
||||||
"theme_primary_accent",
|
"theme_primary_accent",
|
||||||
|
"theme_layout_mode",
|
||||||
]
|
]
|
||||||
|
|
||||||
settings_dict = {}
|
settings_dict = {}
|
||||||
@@ -1893,6 +1919,7 @@ async def get_theme_settings(
|
|||||||
"theme_primary_light": settings_dict.get("theme_primary_light", "#f5d76e"),
|
"theme_primary_light": settings_dict.get("theme_primary_light", "#f5d76e"),
|
||||||
"theme_primary_dark": settings_dict.get("theme_primary_dark", "#c9a227"),
|
"theme_primary_dark": settings_dict.get("theme_primary_dark", "#c9a227"),
|
||||||
"theme_primary_accent": settings_dict.get("theme_primary_accent", "#e8c547"),
|
"theme_primary_accent": settings_dict.get("theme_primary_accent", "#e8c547"),
|
||||||
|
"theme_layout_mode": settings_dict.get("theme_layout_mode", "dark"),
|
||||||
"updated_at": updated_at,
|
"updated_at": updated_at,
|
||||||
"updated_by": updated_by,
|
"updated_by": updated_by,
|
||||||
}
|
}
|
||||||
@@ -1953,6 +1980,14 @@ async def update_theme_settings(
|
|||||||
)
|
)
|
||||||
db_settings["theme_primary_accent"] = request_data.theme_primary_accent
|
db_settings["theme_primary_accent"] = request_data.theme_primary_accent
|
||||||
|
|
||||||
|
if request_data.theme_layout_mode is not None:
|
||||||
|
if request_data.theme_layout_mode not in ["dark", "light"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Invalid theme_layout_mode. Must be 'dark' or 'light'"
|
||||||
|
)
|
||||||
|
db_settings["theme_layout_mode"] = request_data.theme_layout_mode
|
||||||
|
|
||||||
# Update or create settings
|
# Update or create settings
|
||||||
for key, value in db_settings.items():
|
for key, value in db_settings.items():
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
@@ -1975,7 +2010,7 @@ async def update_theme_settings(
|
|||||||
|
|
||||||
# Get updated settings
|
# Get updated settings
|
||||||
updated_settings = {}
|
updated_settings = {}
|
||||||
for key in ["theme_primary_color", "theme_primary_light", "theme_primary_dark", "theme_primary_accent"]:
|
for key in ["theme_primary_color", "theme_primary_light", "theme_primary_dark", "theme_primary_accent", "theme_layout_mode"]:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == key
|
SystemSettings.key == key
|
||||||
).first()
|
).first()
|
||||||
@@ -1988,6 +2023,7 @@ async def update_theme_settings(
|
|||||||
"theme_primary_light": "#f5d76e",
|
"theme_primary_light": "#f5d76e",
|
||||||
"theme_primary_dark": "#c9a227",
|
"theme_primary_dark": "#c9a227",
|
||||||
"theme_primary_accent": "#e8c547",
|
"theme_primary_accent": "#e8c547",
|
||||||
|
"theme_layout_mode": "dark",
|
||||||
}
|
}
|
||||||
updated_settings[key] = defaults.get(key)
|
updated_settings[key] = defaults.get(key)
|
||||||
|
|
||||||
@@ -2009,6 +2045,7 @@ async def update_theme_settings(
|
|||||||
"theme_primary_light": updated_settings.get("theme_primary_light", "#f5d76e"),
|
"theme_primary_light": updated_settings.get("theme_primary_light", "#f5d76e"),
|
||||||
"theme_primary_dark": updated_settings.get("theme_primary_dark", "#c9a227"),
|
"theme_primary_dark": updated_settings.get("theme_primary_dark", "#c9a227"),
|
||||||
"theme_primary_accent": updated_settings.get("theme_primary_accent", "#e8c547"),
|
"theme_primary_accent": updated_settings.get("theme_primary_accent", "#e8c547"),
|
||||||
|
"theme_layout_mode": updated_settings.get("theme_layout_mode", "dark"),
|
||||||
"updated_at": updated_at,
|
"updated_at": updated_at,
|
||||||
"updated_by": updated_by,
|
"updated_by": updated_by,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,22 +307,24 @@ export const notifyPayment = async (
|
|||||||
|
|
||||||
export const generateQRCode = (
|
export const generateQRCode = (
|
||||||
bookingNumber: string,
|
bookingNumber: string,
|
||||||
amount: number
|
amount: number,
|
||||||
|
bankCode?: string,
|
||||||
|
accountNumber?: string,
|
||||||
|
accountName?: string
|
||||||
): string => {
|
): string => {
|
||||||
|
// If bank details are not provided, return empty string (QR code won't be generated)
|
||||||
|
if (!bankCode || !accountNumber || !accountName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const bankCode = 'VCB';
|
|
||||||
const accountNumber = '0123456789';
|
|
||||||
const accountName = 'KHACH SAN ABC';
|
|
||||||
const transferContent = bookingNumber;
|
const transferContent = bookingNumber;
|
||||||
|
|
||||||
|
// Generate QR code URL (using generic QR code service, not Vietnam-specific)
|
||||||
|
// Note: This is a placeholder - you may want to use a different QR code service
|
||||||
|
// that supports international bank transfers
|
||||||
const qrUrl =
|
const qrUrl =
|
||||||
`https://img.vietqr.io/image/${bankCode}-${accountNumber}-compact2.jpg?` +
|
`https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=` +
|
||||||
`amount=${amount}&` +
|
encodeURIComponent(`Bank: ${bankCode}, Account: ${accountNumber}, Name: ${accountName}, Amount: ${amount}, Reference: ${transferContent}`);
|
||||||
`addInfo=${encodeURIComponent(transferContent)}&` +
|
|
||||||
`accountName=${encodeURIComponent(accountName)}`;
|
|
||||||
|
|
||||||
return qrUrl;
|
return qrUrl;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,20 +6,26 @@ import {
|
|||||||
Phone,
|
Phone,
|
||||||
Mail,
|
Mail,
|
||||||
Linkedin,
|
Linkedin,
|
||||||
Twitter
|
Twitter,
|
||||||
|
Clock
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import * as LucideIcons from 'lucide-react';
|
import * as LucideIcons from 'lucide-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import pageContentService from '../services/pageContentService';
|
import pageContentService from '../services/pageContentService';
|
||||||
import type { PageContent } from '../services/pageContentService';
|
import type { PageContent } from '../services/pageContentService';
|
||||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
|
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
|
||||||
|
import { formatWorkingHours } from '../../../shared/utils/format';
|
||||||
|
import { getThemeTextClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
const AboutPage: React.FC = () => {
|
const AboutPage: React.FC = () => {
|
||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||||
const [apiError, setApiError] = useState(false);
|
const [apiError, setApiError] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPageContent = async () => {
|
const fetchPageContent = async () => {
|
||||||
@@ -194,7 +200,9 @@ const AboutPage: React.FC = () => {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-full blur-3xl opacity-40 animate-pulse"></div>
|
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-full blur-3xl opacity-40 animate-pulse"></div>
|
||||||
<div className="relative bg-gradient-to-br from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] p-6 rounded-2xl shadow-2xl shadow-[var(--luxury-gold)]/30">
|
<div className="relative bg-gradient-to-br from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] p-6 rounded-2xl shadow-2xl shadow-[var(--luxury-gold)]/30">
|
||||||
<Hotel className="w-12 h-12 md:w-16 md:h-16 text-white drop-shadow-lg" />
|
{React.createElement(getIconComponent(pageContent?.about_hero_icon, Hotel), {
|
||||||
|
className: 'w-12 h-12 md:w-16 md:h-16 text-white drop-shadow-lg'
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,13 +453,15 @@ const AboutPage: React.FC = () => {
|
|||||||
<p className="text-[var(--luxury-gold)] font-medium mb-4 text-sm tracking-wide uppercase">{member.role}</p>
|
<p className="text-[var(--luxury-gold)] font-medium mb-4 text-sm tracking-wide uppercase">{member.role}</p>
|
||||||
{member.bio && <p className="text-gray-600 text-sm mb-6 leading-relaxed font-light">{member.bio}</p>}
|
{member.bio && <p className="text-gray-600 text-sm mb-6 leading-relaxed font-light">{member.bio}</p>}
|
||||||
{member.social_links && (
|
{member.social_links && (
|
||||||
<div className="flex gap-4 pt-4 border-t border-gray-100">
|
<div className={`flex gap-4 pt-4 border-t ${theme.theme_layout_mode === 'light' ? 'border-gray-200' : 'border-gray-700'}`}>
|
||||||
{member.social_links.linkedin && (
|
{member.social_links.linkedin && (
|
||||||
<a
|
<a
|
||||||
href={member.social_links.linkedin}
|
href={member.social_links.linkedin}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110"
|
className={`w-10 h-10 rounded-full ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gray-100 text-gray-700'
|
||||||
|
: 'bg-gray-800 text-gray-300'} flex items-center justify-center hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110`}
|
||||||
>
|
>
|
||||||
<Linkedin className="w-5 h-5" />
|
<Linkedin className="w-5 h-5" />
|
||||||
</a>
|
</a>
|
||||||
@@ -461,7 +471,9 @@ const AboutPage: React.FC = () => {
|
|||||||
href={member.social_links.twitter}
|
href={member.social_links.twitter}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110"
|
className={`w-10 h-10 rounded-full ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gray-100 text-gray-700'
|
||||||
|
: 'bg-gray-800 text-gray-300'} flex items-center justify-center hover:bg-[var(--luxury-gold)] hover:text-white transition-all duration-300 group-hover:scale-110`}
|
||||||
>
|
>
|
||||||
<Twitter className="w-5 h-5" />
|
<Twitter className="w-5 h-5" />
|
||||||
</a>
|
</a>
|
||||||
@@ -614,7 +626,9 @@ const AboutPage: React.FC = () => {
|
|||||||
{displayAddress && (
|
{displayAddress && (
|
||||||
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2">
|
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2">
|
||||||
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||||
<MapPin className="w-8 h-8 text-white drop-shadow-md" />
|
{React.createElement(getIconComponent(pageContent?.about_contact_icons?.location, MapPin), {
|
||||||
|
className: 'w-8 h-8 text-white drop-shadow-md'
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
|
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
|
||||||
Address
|
Address
|
||||||
@@ -633,7 +647,9 @@ const AboutPage: React.FC = () => {
|
|||||||
{displayPhone && (
|
{displayPhone && (
|
||||||
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2" style={{ animationDelay: '0.1s' }}>
|
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2" style={{ animationDelay: '0.1s' }}>
|
||||||
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||||
<Phone className="w-8 h-8 text-white drop-shadow-md" />
|
{React.createElement(getIconComponent(pageContent?.about_contact_icons?.phone, Phone), {
|
||||||
|
className: 'w-8 h-8 text-white drop-shadow-md'
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
|
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
|
||||||
Phone
|
Phone
|
||||||
@@ -648,7 +664,9 @@ const AboutPage: React.FC = () => {
|
|||||||
{displayEmail && (
|
{displayEmail && (
|
||||||
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2" style={{ animationDelay: '0.2s' }}>
|
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2" style={{ animationDelay: '0.2s' }}>
|
||||||
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||||
<Mail className="w-8 h-8 text-white drop-shadow-md" />
|
{React.createElement(getIconComponent(pageContent?.about_contact_icons?.email, Mail), {
|
||||||
|
className: 'w-8 h-8 text-white drop-shadow-md'
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
|
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
|
||||||
Email
|
Email
|
||||||
@@ -660,6 +678,19 @@ const AboutPage: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
|
||||||
|
<div className="group text-center p-8 bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[var(--luxury-gold)]/40 hover:-translate-y-2" style={{ animationDelay: '0.3s' }}>
|
||||||
|
<div className="w-16 h-16 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--luxury-gold)]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||||
|
<Clock className="w-8 h-8 text-white drop-shadow-md" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-300">
|
||||||
|
Chat Support Hours
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 font-light">
|
||||||
|
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@@ -668,7 +699,9 @@ const AboutPage: React.FC = () => {
|
|||||||
className="group inline-flex items-center space-x-3 px-10 py-4 bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] text-white rounded-xl hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/40 transition-all duration-500 font-medium text-lg tracking-wide relative overflow-hidden"
|
className="group inline-flex items-center space-x-3 px-10 py-4 bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] text-white rounded-xl hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/40 transition-all duration-500 font-medium text-lg tracking-wide relative overflow-hidden"
|
||||||
>
|
>
|
||||||
<span className="relative z-10">Explore Our Rooms</span>
|
<span className="relative z-10">Explore Our Rooms</span>
|
||||||
<Hotel className="w-5 h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300" />
|
{React.createElement(getIconComponent(pageContent?.about_learn_more_icon, Hotel), {
|
||||||
|
className: 'w-5 h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300'
|
||||||
|
})}
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-[var(--luxury-gold-light)] to-[var(--luxury-gold)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
<div className="absolute inset-0 bg-gradient-to-r from-[var(--luxury-gold-light)] to-[var(--luxury-gold)] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
|
|||||||
import pageContentService from '../services/pageContentService';
|
import pageContentService from '../services/pageContentService';
|
||||||
import type { PageContent } from '../services/pageContentService';
|
import type { PageContent } from '../services/pageContentService';
|
||||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
import Loading from '../../../shared/components/Loading';
|
import Loading from '../../../shared/components/Loading';
|
||||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||||
|
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
const AccessibilityPage: React.FC = () => {
|
const AccessibilityPage: React.FC = () => {
|
||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPageContent = async () => {
|
const fetchPageContent = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -26,22 +33,26 @@ const AccessibilityPage: React.FC = () => {
|
|||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = sanitizedContent;
|
tempDiv.innerHTML = sanitizedContent;
|
||||||
|
|
||||||
|
// Get theme-aware colors
|
||||||
|
const isLightMode = theme.theme_layout_mode === 'light';
|
||||||
|
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||||
|
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||||
|
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||||
|
|
||||||
const allElements = tempDiv.querySelectorAll('*');
|
const allElements = tempDiv.querySelectorAll('*');
|
||||||
allElements.forEach((el) => {
|
allElements.forEach((el) => {
|
||||||
const htmlEl = el as HTMLElement;
|
const htmlEl = el as HTMLElement;
|
||||||
const tagName = htmlEl.tagName.toLowerCase();
|
const tagName = htmlEl.tagName.toLowerCase();
|
||||||
const currentColor = htmlEl.style.color;
|
|
||||||
|
|
||||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
// Override inline colors to use theme-aware colors
|
||||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||||
htmlEl.style.color = '#ffffff';
|
htmlEl.style.color = headingColor;
|
||||||
} else if (['strong', 'b'].includes(tagName)) {
|
} else if (['strong', 'b'].includes(tagName)) {
|
||||||
htmlEl.style.color = '#d4af37';
|
htmlEl.style.color = accentColor;
|
||||||
} else if (tagName === 'a') {
|
} else if (tagName === 'a') {
|
||||||
htmlEl.style.color = '#d4af37';
|
htmlEl.style.color = accentColor;
|
||||||
} else {
|
} else {
|
||||||
htmlEl.style.color = '#d1d5db';
|
htmlEl.style.color = bodyColor;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +94,7 @@ const AccessibilityPage: React.FC = () => {
|
|||||||
|
|
||||||
if (!pageContent) {
|
if (!pageContent) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -95,8 +106,8 @@ const AccessibilityPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Accessibility className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
<Accessibility className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Accessibility</h1>
|
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Accessibility</h1>
|
||||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||||
@@ -110,7 +121,7 @@ const AccessibilityPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -133,38 +144,40 @@ const AccessibilityPage: React.FC = () => {
|
|||||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||||
<Accessibility className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
<Accessibility className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||||
|
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||||
{pageContent.title || 'Accessibility'}
|
{pageContent.title || 'Accessibility'}
|
||||||
</h1>
|
</h1>
|
||||||
{pageContent.subtitle && (
|
{pageContent.subtitle && (
|
||||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||||
{pageContent.subtitle}
|
{pageContent.subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||||
<div
|
<div
|
||||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
prose-headings:font-elegant prose-headings:font-semibold
|
||||||
|
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
prose-ul:font-light prose-ul:my-4
|
||||||
|
prose-li:mb-2 prose-li:ml-4
|
||||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
|
||||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
|
||||||
style={{ color: '#d1d5db' }}
|
|
||||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{settings.company_email && (
|
{settings.company_email && (
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
<p className="text-sm text-gray-400 font-light">
|
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||||
For accessibility inquiries, contact us at{' '}
|
For accessibility inquiries, contact us at{' '}
|
||||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||||
{settings.company_email}
|
{settings.company_email}
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import { Calendar, User, Tag, ArrowLeft, Eye, Share2, ArrowRight } from 'lucide-
|
|||||||
import blogService, { BlogPost } from '../services/blogService';
|
import blogService, { BlogPost } from '../services/blogService';
|
||||||
import Loading from '../../../shared/components/Loading';
|
import Loading from '../../../shared/components/Loading';
|
||||||
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
|
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
|
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
const BlogDetailPage: React.FC = () => {
|
const BlogDetailPage: React.FC = () => {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [post, setPost] = useState<BlogPost | null>(null);
|
const [post, setPost] = useState<BlogPost | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [relatedPosts, setRelatedPosts] = useState<BlogPost[]>([]);
|
const [relatedPosts, setRelatedPosts] = useState<BlogPost[]>([]);
|
||||||
|
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (slug) {
|
if (slug) {
|
||||||
fetchPost();
|
fetchPost();
|
||||||
@@ -108,9 +115,9 @@ const BlogDetailPage: React.FC = () => {
|
|||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black text-white flex items-center justify-center">
|
<div className={`min-h-screen ${bgClasses} flex items-center justify-center`}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold mb-4">Post not found</h1>
|
<h1 className={`text-2xl font-bold mb-4 ${textClasses.primary}`}>Post not found</h1>
|
||||||
<Link to="/blog" className="text-[var(--luxury-gold)] hover:underline">
|
<Link to="/blog" className="text-[var(--luxury-gold)] hover:underline">
|
||||||
Back to Blog
|
Back to Blog
|
||||||
</Link>
|
</Link>
|
||||||
@@ -120,7 +127,7 @@ const BlogDetailPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||||
{/* Hero Section with Featured Image */}
|
{/* Hero Section with Featured Image */}
|
||||||
{post.featured_image && (
|
{post.featured_image && (
|
||||||
<div className="relative w-full h-64 sm:h-80 lg:h-96 overflow-hidden">
|
<div className="relative w-full h-64 sm:h-80 lg:h-96 overflow-hidden">
|
||||||
@@ -140,7 +147,7 @@ const BlogDetailPage: React.FC = () => {
|
|||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
<Link
|
<Link
|
||||||
to="/blog"
|
to="/blog"
|
||||||
className="inline-flex items-center gap-2 text-gray-400 hover:text-[var(--luxury-gold)] transition-colors mb-8"
|
className={`inline-flex items-center gap-2 ${textClasses.muted} hover:text-[var(--luxury-gold)] transition-colors mb-8`}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-4 h-4" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
<span>Back to Blog</span>
|
<span>Back to Blog</span>
|
||||||
@@ -173,7 +180,7 @@ const BlogDetailPage: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-6 text-sm text-gray-400 pb-6 border-b border-[var(--luxury-gold)]/20">
|
<div className={`flex flex-wrap items-center gap-6 text-sm ${textClasses.muted} pb-6 border-b border-[var(--luxury-gold)]/20`}>
|
||||||
{post.published_at && (
|
{post.published_at && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
@@ -201,19 +208,18 @@ const BlogDetailPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Article Content */}
|
{/* Article Content */}
|
||||||
<article className="prose prose-invert prose-lg max-w-none mb-12
|
<article className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none mb-12
|
||||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
prose-headings:font-elegant prose-headings:font-semibold
|
||||||
prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-6 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-3
|
prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-6 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-3
|
||||||
prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4
|
prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4
|
||||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-6
|
prose-p:font-light prose-p:leading-relaxed prose-p:mb-6
|
||||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-6
|
prose-ul:font-light prose-ul:my-6
|
||||||
prose-ol:text-gray-300 prose-ol:font-light prose-ol:my-6
|
prose-ol:font-light prose-ol:my-6
|
||||||
prose-li:text-gray-300 prose-li:mb-2
|
prose-li:mb-2
|
||||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
||||||
prose-img:rounded-xl prose-img:shadow-2xl
|
prose-img:rounded-xl prose-img:shadow-2xl`}
|
||||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white
|
style={{ color: theme.theme_layout_mode === 'light' ? '#374151' : '#d1d5db' }}
|
||||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||||
@@ -245,7 +251,7 @@ const BlogDetailPage: React.FC = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
{section.content && (
|
{section.content && (
|
||||||
<p className="text-xl md:text-2xl text-gray-200 font-light leading-relaxed max-w-3xl mx-auto">
|
<p className={`text-xl md:text-2xl ${textClasses.secondary} font-light leading-relaxed max-w-3xl mx-auto`}>
|
||||||
{section.content}
|
{section.content}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -277,8 +283,8 @@ const BlogDetailPage: React.FC = () => {
|
|||||||
<div className="rounded-3xl overflow-hidden border-2 border-[var(--luxury-gold)]/20 shadow-2xl">
|
<div className="rounded-3xl overflow-hidden border-2 border-[var(--luxury-gold)]/20 shadow-2xl">
|
||||||
<img src={section.image} alt={section.title || 'Blog image'} className="w-full h-auto" />
|
<img src={section.image} alt={section.title || 'Blog image'} className="w-full h-auto" />
|
||||||
{section.title && (
|
{section.title && (
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] px-6 py-4 border-t border-[var(--luxury-gold)]/20">
|
<div className={`${cardClasses} px-6 py-4 border-t border-[var(--luxury-gold)]/20`}>
|
||||||
<p className="text-gray-400 text-sm font-light italic text-center">{section.title}</p>
|
<p className={`${textClasses.muted} text-sm font-light italic text-center`}>{section.title}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -373,7 +379,7 @@ const BlogDetailPage: React.FC = () => {
|
|||||||
{/* Related Posts */}
|
{/* Related Posts */}
|
||||||
{relatedPosts.length > 0 && (
|
{relatedPosts.length > 0 && (
|
||||||
<div className="mt-16 pt-12 border-t border-[var(--luxury-gold)]/20">
|
<div className="mt-16 pt-12 border-t border-[var(--luxury-gold)]/20">
|
||||||
<h2 className="text-2xl sm:text-3xl font-bold text-white mb-8">Related Posts</h2>
|
<h2 className={`text-2xl sm:text-3xl font-bold ${textClasses.primary} mb-8`}>Related Posts</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
{relatedPosts.map((relatedPost) => (
|
{relatedPosts.map((relatedPost) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -391,11 +397,11 @@ const BlogDetailPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<h3 className="text-lg font-bold text-white mb-2 group-hover:text-[var(--luxury-gold)] transition-colors line-clamp-2">
|
<h3 className={`text-lg font-bold ${textClasses.primary} mb-2 group-hover:text-[var(--luxury-gold)] transition-colors line-clamp-2`}>
|
||||||
{relatedPost.title}
|
{relatedPost.title}
|
||||||
</h3>
|
</h3>
|
||||||
{relatedPost.excerpt && (
|
{relatedPost.excerpt && (
|
||||||
<p className="text-gray-400 text-sm line-clamp-2 font-light">
|
<p className={`${textClasses.muted} text-sm line-clamp-2 font-light`}>
|
||||||
{relatedPost.excerpt}
|
{relatedPost.excerpt}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import { Link } from 'react-router-dom';
|
|||||||
import { Calendar, User, Tag, Search, ArrowRight, Eye, Sparkles, BookOpen } from 'lucide-react';
|
import { Calendar, User, Tag, Search, ArrowRight, Eye, Sparkles, BookOpen } from 'lucide-react';
|
||||||
import blogService, { BlogPost } from '../services/blogService';
|
import blogService, { BlogPost } from '../services/blogService';
|
||||||
import Loading from '../../../shared/components/Loading';
|
import Loading from '../../../shared/components/Loading';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
|
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
|
||||||
import Pagination from '../../../shared/components/Pagination';
|
import Pagination from '../../../shared/components/Pagination';
|
||||||
|
|
||||||
const BlogPage: React.FC = () => {
|
const BlogPage: React.FC = () => {
|
||||||
|
const { theme } = useTheme();
|
||||||
const [posts, setPosts] = useState<BlogPost[]>([]);
|
const [posts, setPosts] = useState<BlogPost[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
@@ -67,10 +70,16 @@ const BlogPage: React.FC = () => {
|
|||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8 overflow-hidden relative">
|
<div className={`w-full ${heroBgClasses} pt-6 sm:pt-7 md:pt-8 overflow-hidden relative`}>
|
||||||
{/* Background Effects */}
|
{/* Background Effects */}
|
||||||
<div className="absolute inset-0 opacity-10">
|
<div className="absolute inset-0 opacity-10">
|
||||||
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
||||||
@@ -83,18 +92,20 @@ const BlogPage: React.FC = () => {
|
|||||||
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
|
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
|
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
|
||||||
<div className="relative p-2 sm:p-2.5 md:p-3 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300">
|
<div className={`relative p-2 sm:p-2.5 md:p-3 ${cardClasses} rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300`}>
|
||||||
<BookOpen className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg" />
|
<BookOpen className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
|
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
|
||||||
<span className="bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
<span className={`${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||||
|
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||||
Our Blog
|
Our Blog
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
|
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
|
||||||
<p className="text-sm sm:text-base md:text-lg text-gray-300 font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4">
|
<p className={`text-sm sm:text-base md:text-lg ${textClasses.secondary} font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4`}>
|
||||||
Discover stories, insights, and updates from our luxury hotel
|
Discover stories, insights, and updates from our luxury hotel
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 flex items-center justify-center gap-2 text-[var(--luxury-gold)]/60">
|
<div className="mt-4 flex items-center justify-center gap-2 text-[var(--luxury-gold)]/60">
|
||||||
@@ -125,7 +136,7 @@ const BlogPage: React.FC = () => {
|
|||||||
setSearchTerm(e.target.value);
|
setSearchTerm(e.target.value);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
className="w-full pl-14 pr-5 py-4 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] border border-[var(--luxury-gold)]/20 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light"
|
className={`w-full pl-14 pr-5 py-4 ${inputClasses} border border-[var(--luxury-gold)]/20 rounded-xl placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +150,7 @@ const BlogPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* Results Count */}
|
{/* Results Count */}
|
||||||
{!loading && total > 0 && (
|
{!loading && total > 0 && (
|
||||||
<div className="mb-8 text-gray-400 font-light text-sm">
|
<div className={`mb-8 ${textClasses.muted} font-light text-sm`}>
|
||||||
Showing {posts.length} of {total} posts
|
Showing {posts.length} of {total} posts
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -150,8 +161,8 @@ const BlogPage: React.FC = () => {
|
|||||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--luxury-gold)]/10 mb-6">
|
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--luxury-gold)]/10 mb-6">
|
||||||
<BookOpen className="w-10 h-10 text-[var(--luxury-gold)]" />
|
<BookOpen className="w-10 h-10 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-400 text-xl font-light">No blog posts found</p>
|
<p className={`${textClasses.muted} text-xl font-light`}>No blog posts found</p>
|
||||||
<p className="text-gray-500 text-sm mt-2">Try adjusting your search or filters</p>
|
<p className={`${textClasses.muted} text-sm mt-2`}>Try adjusting your search or filters</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -160,7 +171,7 @@ const BlogPage: React.FC = () => {
|
|||||||
<Link
|
<Link
|
||||||
key={post.id}
|
key={post.id}
|
||||||
to={`/blog/${post.slug}`}
|
to={`/blog/${post.slug}`}
|
||||||
className="group relative bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3"
|
className={`group relative ${cardClasses} rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3`}
|
||||||
style={{ animationDelay: `${index * 50}ms` }}
|
style={{ animationDelay: `${index * 50}ms` }}
|
||||||
>
|
>
|
||||||
{/* Premium Glow Effects */}
|
{/* Premium Glow Effects */}
|
||||||
@@ -204,15 +215,15 @@ const BlogPage: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h2 className="text-2xl sm:text-3xl font-serif font-bold text-white mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight">
|
<h2 className={`text-2xl sm:text-3xl font-serif font-bold ${textClasses.primary} mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight`}>
|
||||||
{post.title}
|
{post.title}
|
||||||
</h2>
|
</h2>
|
||||||
{post.excerpt && (
|
{post.excerpt && (
|
||||||
<p className="text-gray-300 text-base mb-6 line-clamp-3 font-light leading-relaxed">
|
<p className={`${textClasses.secondary} text-base mb-6 line-clamp-3 font-light leading-relaxed`}>
|
||||||
{post.excerpt}
|
{post.excerpt}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between text-sm text-gray-400 pt-6 border-t border-[var(--luxury-gold)]/20 mb-6">
|
<div className={`flex items-center justify-between text-sm ${textClasses.muted} pt-6 border-t border-[var(--luxury-gold)]/20 mb-6`}>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{post.published_at && (
|
{post.published_at && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -259,10 +270,10 @@ const BlogPage: React.FC = () => {
|
|||||||
<div className="lg:col-span-3">
|
<div className="lg:col-span-3">
|
||||||
{allTags.length > 0 && (
|
{allTags.length > 0 && (
|
||||||
<div className="sticky top-8">
|
<div className="sticky top-8">
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 backdrop-blur-xl shadow-2xl">
|
<div className={`${cardClasses} rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 backdrop-blur-xl shadow-2xl`}>
|
||||||
<div className="flex items-center gap-3 mb-6 pb-6 border-b border-[var(--luxury-gold)]/20">
|
<div className="flex items-center gap-3 mb-6 pb-6 border-b border-[var(--luxury-gold)]/20">
|
||||||
<div className="w-1 h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
|
<div className="w-1 h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
|
||||||
<h3 className="text-xl font-serif font-bold text-white">Filter by Tags</h3>
|
<h3 className={`text-xl font-serif font-bold ${textClasses.primary}`}>Filter by Tags</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<button
|
<button
|
||||||
@@ -273,7 +284,7 @@ const BlogPage: React.FC = () => {
|
|||||||
className={`group relative w-full text-left px-5 py-4 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
className={`group relative w-full text-left px-5 py-4 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
||||||
selectedTag === null
|
selectedTag === null
|
||||||
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
||||||
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] hover:bg-[#1a1a1a]'
|
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="relative z-10 flex items-center gap-2">
|
<span className="relative z-10 flex items-center gap-2">
|
||||||
@@ -294,7 +305,7 @@ const BlogPage: React.FC = () => {
|
|||||||
className={`group relative w-full text-left px-5 py-4 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
className={`group relative w-full text-left px-5 py-4 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
||||||
selectedTag === tag
|
selectedTag === tag
|
||||||
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
||||||
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] hover:bg-[#1a1a1a]'
|
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="relative z-10 flex items-center gap-2">
|
<span className="relative z-10 flex items-center gap-2">
|
||||||
|
|||||||
@@ -5,12 +5,19 @@ import pageContentService, { PageContent } from '../services/pageContentService'
|
|||||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||||
import Loading from '../../../shared/components/Loading';
|
import Loading from '../../../shared/components/Loading';
|
||||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
|
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
const CancellationPolicyPage: React.FC = () => {
|
const CancellationPolicyPage: React.FC = () => {
|
||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPageContent = async () => {
|
const fetchPageContent = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -26,22 +33,27 @@ const CancellationPolicyPage: React.FC = () => {
|
|||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = sanitizedContent;
|
tempDiv.innerHTML = sanitizedContent;
|
||||||
|
|
||||||
|
// Get theme-aware colors
|
||||||
|
const isLightMode = theme.theme_layout_mode === 'light';
|
||||||
|
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||||
|
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||||
|
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||||
|
|
||||||
const allElements = tempDiv.querySelectorAll('*');
|
const allElements = tempDiv.querySelectorAll('*');
|
||||||
allElements.forEach((el) => {
|
allElements.forEach((el) => {
|
||||||
const htmlEl = el as HTMLElement;
|
const htmlEl = el as HTMLElement;
|
||||||
const tagName = htmlEl.tagName.toLowerCase();
|
const tagName = htmlEl.tagName.toLowerCase();
|
||||||
const currentColor = htmlEl.style.color;
|
const currentColor = htmlEl.style.color;
|
||||||
|
|
||||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
// Override inline colors to use theme-aware colors
|
||||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||||
htmlEl.style.color = '#ffffff';
|
htmlEl.style.color = headingColor;
|
||||||
} else if (['strong', 'b'].includes(tagName)) {
|
} else if (['strong', 'b'].includes(tagName)) {
|
||||||
htmlEl.style.color = '#d4af37';
|
htmlEl.style.color = accentColor;
|
||||||
} else if (tagName === 'a') {
|
} else if (tagName === 'a') {
|
||||||
htmlEl.style.color = '#d4af37';
|
htmlEl.style.color = accentColor;
|
||||||
} else {
|
} else {
|
||||||
htmlEl.style.color = '#d1d5db';
|
htmlEl.style.color = bodyColor;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +95,7 @@ const CancellationPolicyPage: React.FC = () => {
|
|||||||
|
|
||||||
if (!pageContent) {
|
if (!pageContent) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -95,8 +107,8 @@ const CancellationPolicyPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<XCircle className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
<XCircle className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Cancellation Policy</h1>
|
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Cancellation Policy</h1>
|
||||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||||
@@ -110,7 +122,7 @@ const CancellationPolicyPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -133,38 +145,42 @@ const CancellationPolicyPage: React.FC = () => {
|
|||||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||||
<XCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
<XCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||||
|
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||||
{pageContent.title || 'Cancellation Policy'}
|
{pageContent.title || 'Cancellation Policy'}
|
||||||
</h1>
|
</h1>
|
||||||
{pageContent.subtitle && (
|
{pageContent.subtitle && (
|
||||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||||
{pageContent.subtitle}
|
{pageContent.subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||||
<div
|
<div
|
||||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
prose-headings:font-elegant prose-headings:font-semibold
|
||||||
|
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
prose-ul:font-light prose-ul:my-4
|
||||||
|
prose-li:mb-2 prose-li:ml-4
|
||||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
style={{
|
||||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db',
|
||||||
style={{ color: '#d1d5db' }}
|
}}
|
||||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{settings.company_email && (
|
{settings.company_email && (
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
<p className="text-sm text-gray-400 font-light">
|
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||||
For questions about cancellations, contact us at{' '}
|
For questions about cancellations, contact us at{' '}
|
||||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||||
{settings.company_email}
|
{settings.company_email}
|
||||||
|
|||||||
@@ -1,18 +1,41 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Mail, Phone, MapPin, Send, User, MessageSquare } from 'lucide-react';
|
import { Mail, Phone, MapPin, Send, User, MessageSquare, Clock } from 'lucide-react';
|
||||||
|
import * as LucideIcons from 'lucide-react';
|
||||||
import { submitContactForm } from '../services/contactService';
|
import { submitContactForm } from '../services/contactService';
|
||||||
import pageContentService from '../services/pageContentService';
|
import pageContentService from '../services/pageContentService';
|
||||||
import type { PageContent } from '../services/pageContentService';
|
import type { PageContent } from '../services/pageContentService';
|
||||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import Recaptcha from '../../../shared/components/Recaptcha';
|
import Recaptcha from '../../../shared/components/Recaptcha';
|
||||||
import { recaptchaService } from '../../../features/system/services/systemSettingsService';
|
import { recaptchaService } from '../../../features/system/services/systemSettingsService';
|
||||||
import ChatWidget from '../../notifications/components/ChatWidget';
|
import ChatWidget from '../../notifications/components/ChatWidget';
|
||||||
import { useAntibotForm } from '../../../features/auth/hooks/useAntibotForm';
|
import { useAntibotForm } from '../../../features/auth/hooks/useAntibotForm';
|
||||||
import HoneypotField from '../../../shared/components/HoneypotField';
|
import HoneypotField from '../../../shared/components/HoneypotField';
|
||||||
|
import { formatWorkingHours } from '../../../shared/utils/format';
|
||||||
|
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
|
// Helper function to get icon component from icon name (handles both PascalCase and lowercase)
|
||||||
|
const getIconComponent = (iconName?: string, fallback: any = Mail) => {
|
||||||
|
if (!iconName) return fallback;
|
||||||
|
|
||||||
|
// Try direct match first (for PascalCase names)
|
||||||
|
if ((LucideIcons as any)[iconName]) {
|
||||||
|
return (LucideIcons as any)[iconName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to PascalCase (capitalize first letter)
|
||||||
|
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
|
||||||
|
if ((LucideIcons as any)[pascalCaseName]) {
|
||||||
|
return (LucideIcons as any)[pascalCaseName];
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
const ContactPage: React.FC = () => {
|
const ContactPage: React.FC = () => {
|
||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -153,9 +176,9 @@ const ContactPage: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const displayPhone = settings.company_phone || 'Available 24/7 for your convenience';
|
const displayPhone = settings.company_phone || null;
|
||||||
const displayEmail = settings.company_email || "We'll respond within 24 hours";
|
const displayEmail = settings.company_email || null;
|
||||||
const displayAddress = settings.company_address || 'Visit us at our hotel reception';
|
const displayAddress = settings.company_address || null;
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
@@ -171,10 +194,16 @@ const ContactPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||||
{}
|
{}
|
||||||
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8 overflow-hidden relative">
|
<div className={`w-full ${heroBgClasses} pt-6 sm:pt-7 md:pt-8 overflow-hidden relative`}>
|
||||||
{}
|
{}
|
||||||
<div className="absolute inset-0 opacity-10">
|
<div className="absolute inset-0 opacity-10">
|
||||||
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
||||||
@@ -187,18 +216,22 @@ const ContactPage: React.FC = () => {
|
|||||||
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
|
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
|
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
|
||||||
<div className="relative p-2 sm:p-2.5 md:p-3 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300">
|
<div className={`relative p-2 sm:p-2.5 md:p-3 ${cardClasses} rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300`}>
|
||||||
<Mail className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg" />
|
{React.createElement(getIconComponent(pageContent?.contact_icons?.hero, Mail), {
|
||||||
|
className: 'w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
|
<h1 className={`text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2`}>
|
||||||
<span className="bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
<span className={theme.theme_layout_mode === 'light'
|
||||||
|
? "bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900 bg-clip-text text-transparent"
|
||||||
|
: "bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent"}>
|
||||||
{pageContent?.title || 'Contact Us'}
|
{pageContent?.title || 'Contact Us'}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
|
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
|
||||||
<p className="text-sm sm:text-base md:text-lg text-gray-300 font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4">
|
<p className={`text-sm sm:text-base md:text-lg ${textClasses.secondary} font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4`}>
|
||||||
{pageContent?.subtitle || pageContent?.description || "Experience the pinnacle of hospitality. We're here to make your stay extraordinary."}
|
{pageContent?.subtitle || pageContent?.description || "Experience the pinnacle of hospitality. We're here to make your stay extraordinary."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,18 +245,18 @@ const ContactPage: React.FC = () => {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 sm:gap-5 md:gap-6 lg:gap-7 xl:gap-8 2xl:gap-10 max-w-7xl mx-auto">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 sm:gap-5 md:gap-6 lg:gap-7 xl:gap-8 2xl:gap-10 max-w-7xl mx-auto">
|
||||||
{}
|
{}
|
||||||
<div className="lg:col-span-4">
|
<div className="lg:col-span-4">
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a]
|
<div className={`${cardClasses}
|
||||||
rounded-xl sm:rounded-2xl border-2 border-[var(--luxury-gold)]/30 p-5 sm:p-6 md:p-8 lg:p-10
|
rounded-xl sm:rounded-2xl border-2 border-[var(--luxury-gold)]/30 p-5 sm:p-6 md:p-8 lg:p-10
|
||||||
shadow-2xl shadow-[var(--luxury-gold)]/10 backdrop-blur-xl
|
shadow-2xl shadow-[var(--luxury-gold)]/10 backdrop-blur-xl
|
||||||
relative overflow-hidden h-full group hover:border-[var(--luxury-gold)]/50 transition-all duration-500">
|
relative overflow-hidden h-full group hover:border-[var(--luxury-gold)]/50 transition-all duration-500`}>
|
||||||
{}
|
{}
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)]/5 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)]/5 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
|
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8">
|
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8">
|
||||||
<div className="w-0.5 sm:w-1 h-6 sm:h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
|
<div className="w-0.5 sm:w-1 h-6 sm:h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
|
||||||
<h2 className="text-xl sm:text-2xl md:text-3xl font-serif font-semibold
|
<h2 className={`text-xl sm:text-2xl md:text-3xl font-serif font-semibold
|
||||||
text-white tracking-tight">
|
${textClasses.primary} tracking-tight`}>
|
||||||
Get in Touch
|
Get in Touch
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,11 +264,13 @@ const ContactPage: React.FC = () => {
|
|||||||
<div className="space-y-5 sm:space-y-6 md:space-y-7">
|
<div className="space-y-5 sm:space-y-6 md:space-y-7">
|
||||||
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
||||||
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
|
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
|
||||||
<Mail className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
|
{React.createElement(getIconComponent(pageContent?.contact_icons?.email, Mail), {
|
||||||
|
className: 'w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Email</h3>
|
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Email</h3>
|
||||||
<a href={`mailto:${displayEmail}`} className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors">
|
<a href={`mailto:${displayEmail}`} className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors`}>
|
||||||
{displayEmail}
|
{displayEmail}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,11 +278,13 @@ const ContactPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
||||||
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
|
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
|
||||||
<Phone className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
|
{React.createElement(getIconComponent(pageContent?.contact_icons?.phone, Phone), {
|
||||||
|
className: 'w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Phone</h3>
|
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Phone</h3>
|
||||||
<a href={`tel:${displayPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`} className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors">
|
<a href={`tel:${displayPhone?.replace(/\s+/g, '').replace(/[()]/g, '') || ''}`} className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed hover:text-[var(--luxury-gold)] transition-colors`}>
|
||||||
{displayPhone}
|
{displayPhone}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,15 +292,31 @@ const ContactPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
||||||
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
|
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
|
||||||
<MapPin className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
|
{React.createElement(getIconComponent(pageContent?.contact_icons?.location, MapPin), {
|
||||||
|
className: 'w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Location</h3>
|
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Location</h3>
|
||||||
<p className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed whitespace-pre-line">
|
<p className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed whitespace-pre-line`}>
|
||||||
{displayAddress}
|
{displayAddress}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
|
||||||
|
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
||||||
|
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold)]/10 rounded-lg sm:rounded-xl border border-[var(--luxury-gold)]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[var(--luxury-gold)]/30 group-hover/item:to-[var(--luxury-gold)]/20 group-hover/item:border-[var(--luxury-gold)]/60 transition-all duration-300 shadow-lg shadow-[var(--luxury-gold)]/10">
|
||||||
|
<Clock className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm sm:text-base font-medium text-[var(--luxury-gold)] mb-1 sm:mb-2 tracking-wide">Chat Support Hours</h3>
|
||||||
|
<p className={`${textClasses.secondary} font-light text-xs sm:text-sm leading-relaxed`}>
|
||||||
|
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
{}
|
||||||
@@ -289,7 +342,7 @@ const ContactPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-5 sm:mt-6 md:mt-7 pt-5 sm:pt-6 md:pt-7 border-t border-[var(--luxury-gold)]/30">
|
<div className="mt-5 sm:mt-6 md:mt-7 pt-5 sm:pt-6 md:pt-7 border-t border-[var(--luxury-gold)]/30">
|
||||||
<p className="text-gray-400 font-light text-xs sm:text-sm leading-relaxed tracking-wide">
|
<p className={`${textClasses.muted} font-light text-xs sm:text-sm leading-relaxed tracking-wide`}>
|
||||||
{pageContent?.content || "Our team is here to help you with any questions about your stay, bookings, or special requests. We're committed to exceeding your expectations."}
|
{pageContent?.content || "Our team is here to help you with any questions about your stay, bookings, or special requests. We're committed to exceeding your expectations."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,10 +352,10 @@ const ContactPage: React.FC = () => {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
<div className="lg:col-span-8">
|
<div className="lg:col-span-8">
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a]
|
<div className={`${cardClasses}
|
||||||
rounded-xl sm:rounded-2xl border-2 border-[var(--luxury-gold)]/30 p-5 sm:p-6 md:p-8 lg:p-10
|
rounded-xl sm:rounded-2xl border-2 border-[var(--luxury-gold)]/30 p-5 sm:p-6 md:p-8 lg:p-10
|
||||||
shadow-2xl shadow-[var(--luxury-gold)]/10 backdrop-blur-xl
|
shadow-2xl shadow-[var(--luxury-gold)]/10 backdrop-blur-xl
|
||||||
relative overflow-hidden">
|
relative overflow-hidden`}>
|
||||||
{}
|
{}
|
||||||
<div className="absolute inset-0 opacity-5">
|
<div className="absolute inset-0 opacity-5">
|
||||||
<div className="absolute top-0 right-0 w-48 sm:w-64 md:w-96 h-48 sm:h-64 md:h-96 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
<div className="absolute top-0 right-0 w-48 sm:w-64 md:w-96 h-48 sm:h-64 md:h-96 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
||||||
@@ -311,8 +364,8 @@ const ContactPage: React.FC = () => {
|
|||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8">
|
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8">
|
||||||
<div className="w-0.5 sm:w-1 h-6 sm:h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
|
<div className="w-0.5 sm:w-1 h-6 sm:h-8 bg-gradient-to-b from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full"></div>
|
||||||
<h2 className="text-xl sm:text-2xl md:text-3xl font-serif font-semibold
|
<h2 className={`text-xl sm:text-2xl md:text-3xl font-serif font-semibold
|
||||||
text-white tracking-tight">
|
${textClasses.primary} tracking-tight`}>
|
||||||
Send Us a Message
|
Send Us a Message
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,9 +381,11 @@ const ContactPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
{}
|
{}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="name" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
<label htmlFor="name" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
|
||||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||||
<User className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
|
{React.createElement(getIconComponent(pageContent?.contact_icons?.name_field, User), {
|
||||||
|
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||||
|
})}
|
||||||
Full Name <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
Full Name <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -340,8 +395,8 @@ const ContactPage: React.FC = () => {
|
|||||||
name="name"
|
name="name"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
|
||||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
text-sm sm:text-base placeholder-gray-500/60
|
||||||
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
||||||
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
||||||
${errors.name ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
${errors.name ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
||||||
@@ -356,9 +411,11 @@ const ContactPage: React.FC = () => {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-5 md:gap-6 lg:gap-7">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-5 md:gap-6 lg:gap-7">
|
||||||
{}
|
{}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
<label htmlFor="email" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
|
||||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||||
<Mail className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
|
{React.createElement(getIconComponent(pageContent?.contact_icons?.email_field, Mail), {
|
||||||
|
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||||
|
})}
|
||||||
Email <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
Email <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -368,8 +425,8 @@ const ContactPage: React.FC = () => {
|
|||||||
name="email"
|
name="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
|
||||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
text-sm sm:text-base placeholder-gray-500/60
|
||||||
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
||||||
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
||||||
${errors.email ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
${errors.email ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
||||||
@@ -382,10 +439,12 @@ const ContactPage: React.FC = () => {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="phone" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
<label htmlFor="phone" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
|
||||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||||
<Phone className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
|
{React.createElement(getIconComponent(pageContent?.contact_icons?.phone_field, Phone), {
|
||||||
Phone <span className="text-gray-500 text-xs">(Optional)</span>
|
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||||
|
})}
|
||||||
|
Phone <span className={`${textClasses.muted} text-xs`}>(Optional)</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -394,10 +453,10 @@ const ContactPage: React.FC = () => {
|
|||||||
name="phone"
|
name="phone"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 border-[var(--luxury-gold)]/30 rounded-lg
|
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 border-[var(--luxury-gold)]/30 rounded-lg
|
||||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
text-sm sm:text-base placeholder-gray-500/60
|
||||||
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
||||||
transition-all duration-300 hover:border-[var(--luxury-gold)]/40"
|
transition-all duration-300 hover:border-[var(--luxury-gold)]/40`}
|
||||||
placeholder="+1 (555) 123-4567"
|
placeholder="+1 (555) 123-4567"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -405,9 +464,11 @@ const ContactPage: React.FC = () => {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="subject" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
<label htmlFor="subject" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
|
||||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||||
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
|
{React.createElement(getIconComponent(pageContent?.contact_icons?.subject_field, MessageSquare), {
|
||||||
|
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||||
|
})}
|
||||||
Subject <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
Subject <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -417,8 +478,8 @@ const ContactPage: React.FC = () => {
|
|||||||
name="subject"
|
name="subject"
|
||||||
value={formData.subject}
|
value={formData.subject}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
|
||||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
text-sm sm:text-base placeholder-gray-500/60
|
||||||
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
||||||
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
||||||
${errors.subject ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
${errors.subject ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
||||||
@@ -431,9 +492,11 @@ const ContactPage: React.FC = () => {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="message" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
<label htmlFor="message" className={`block text-xs sm:text-sm font-medium ${textClasses.secondary} mb-2 sm:mb-3 tracking-wide`}>
|
||||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||||
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg" />
|
{React.createElement(getIconComponent(pageContent?.contact_icons?.message_field, MessageSquare), {
|
||||||
|
className: 'w-3.5 h-3.5 sm:w-4 sm:h-4 text-[var(--luxury-gold)] drop-shadow-lg'
|
||||||
|
})}
|
||||||
Message <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
Message <span className="text-[var(--luxury-gold)] font-semibold">*</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -443,8 +506,8 @@ const ContactPage: React.FC = () => {
|
|||||||
value={formData.message}
|
value={formData.message}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
rows={6}
|
rows={6}
|
||||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
className={`w-full px-4 sm:px-5 py-3 sm:py-4 ${inputClasses} border-2 rounded-lg
|
||||||
text-white text-sm sm:text-base placeholder-gray-500/60 resize-none
|
text-sm sm:text-base placeholder-gray-500/60 resize-none
|
||||||
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)] focus:shadow-lg focus:shadow-[var(--luxury-gold)]/20
|
||||||
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
transition-all duration-300 hover:border-[var(--luxury-gold)]/40
|
||||||
${errors.message ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
${errors.message ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[var(--luxury-gold)]/30'}`}
|
||||||
@@ -494,7 +557,9 @@ const ContactPage: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Send className="w-4 h-4 sm:w-5 sm:h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300" />
|
{React.createElement(getIconComponent(pageContent?.contact_icons?.submit_button, Send), {
|
||||||
|
className: 'w-4 h-4 sm:w-5 sm:h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300'
|
||||||
|
})}
|
||||||
<span className="relative z-10">Send Message</span>
|
<span className="relative z-10">Send Message</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
|
|||||||
import pageContentService from '../services/pageContentService';
|
import pageContentService from '../services/pageContentService';
|
||||||
import type { PageContent } from '../services/pageContentService';
|
import type { PageContent } from '../services/pageContentService';
|
||||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
import Loading from '../../../shared/components/Loading';
|
import Loading from '../../../shared/components/Loading';
|
||||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||||
|
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
const FAQPage: React.FC = () => {
|
const FAQPage: React.FC = () => {
|
||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPageContent = async () => {
|
const fetchPageContent = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -26,22 +33,26 @@ const FAQPage: React.FC = () => {
|
|||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = sanitizedContent;
|
tempDiv.innerHTML = sanitizedContent;
|
||||||
|
|
||||||
|
// Get theme-aware colors
|
||||||
|
const isLightMode = theme.theme_layout_mode === 'light';
|
||||||
|
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||||
|
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||||
|
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||||
|
|
||||||
|
// Override inline colors to use theme-aware colors
|
||||||
const allElements = tempDiv.querySelectorAll('*');
|
const allElements = tempDiv.querySelectorAll('*');
|
||||||
allElements.forEach((el) => {
|
allElements.forEach((el) => {
|
||||||
const htmlEl = el as HTMLElement;
|
const htmlEl = el as HTMLElement;
|
||||||
const tagName = htmlEl.tagName.toLowerCase();
|
const tagName = htmlEl.tagName.toLowerCase();
|
||||||
const currentColor = htmlEl.style.color;
|
|
||||||
|
|
||||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
htmlEl.style.color = headingColor;
|
||||||
htmlEl.style.color = '#ffffff';
|
} else if (['strong', 'b'].includes(tagName)) {
|
||||||
} else if (['strong', 'b'].includes(tagName)) {
|
htmlEl.style.color = accentColor;
|
||||||
htmlEl.style.color = '#d4af37';
|
} else if (tagName === 'a') {
|
||||||
} else if (tagName === 'a') {
|
htmlEl.style.color = accentColor;
|
||||||
htmlEl.style.color = '#d4af37';
|
} else {
|
||||||
} else {
|
htmlEl.style.color = bodyColor;
|
||||||
htmlEl.style.color = '#d1d5db';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +94,7 @@ const FAQPage: React.FC = () => {
|
|||||||
|
|
||||||
if (!pageContent) {
|
if (!pageContent) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -95,8 +106,8 @@ const FAQPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<HelpCircle className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
<HelpCircle className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Frequently Asked Questions</h1>
|
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Frequently Asked Questions</h1>
|
||||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||||
@@ -110,7 +121,7 @@ const FAQPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -133,38 +144,40 @@ const FAQPage: React.FC = () => {
|
|||||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||||
<HelpCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
<HelpCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900 bg-clip-text text-transparent'
|
||||||
|
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent'}`}>
|
||||||
{pageContent.title || 'Frequently Asked Questions'}
|
{pageContent.title || 'Frequently Asked Questions'}
|
||||||
</h1>
|
</h1>
|
||||||
{pageContent.subtitle && (
|
{pageContent.subtitle && (
|
||||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||||
{pageContent.subtitle}
|
{pageContent.subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||||
<div
|
<div
|
||||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
prose-headings:font-elegant prose-headings:font-semibold
|
||||||
|
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
prose-ul:font-light prose-ul:my-4
|
||||||
|
prose-li:mb-2 prose-li:ml-4
|
||||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
|
||||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
|
||||||
style={{ color: '#d1d5db' }}
|
|
||||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{settings.company_email && (
|
{settings.company_email && (
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
<p className="text-sm text-gray-400 font-light">
|
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||||
Still have questions? Contact us at{' '}
|
Still have questions? Contact us at{' '}
|
||||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||||
{settings.company_email}
|
{settings.company_email}
|
||||||
|
|||||||
@@ -29,6 +29,24 @@ import type { Room } from '../../rooms/services/roomService';
|
|||||||
import type { PageContent } from '../services/pageContentService';
|
import type { PageContent } from '../services/pageContentService';
|
||||||
import type { Service } from '../../hotel_services/services/serviceService';
|
import type { Service } from '../../hotel_services/services/serviceService';
|
||||||
|
|
||||||
|
// Helper function to get icon component from icon name (handles both PascalCase and lowercase)
|
||||||
|
const getIconComponent = (iconName?: string) => {
|
||||||
|
if (!iconName) return null;
|
||||||
|
|
||||||
|
// Try direct match first (for PascalCase names)
|
||||||
|
if ((LucideIcons as any)[iconName]) {
|
||||||
|
return (LucideIcons as any)[iconName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to PascalCase (capitalize first letter)
|
||||||
|
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
|
||||||
|
if ((LucideIcons as any)[pascalCaseName]) {
|
||||||
|
return (LucideIcons as any)[pascalCaseName];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const HomePage: React.FC = () => {
|
const HomePage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { formatCurrency } = useFormatCurrency();
|
const { formatCurrency } = useFormatCurrency();
|
||||||
@@ -1081,31 +1099,34 @@ const HomePage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6 lg:gap-8">
|
<div className="relative grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6 lg:gap-8">
|
||||||
{pageContent.stats.map((stat, index) => (
|
{pageContent.stats.map((stat, index) => {
|
||||||
<div key={`stat-${index}-${stat.label || index}`} className="text-center group relative">
|
const IconComponent = stat?.icon ? getIconComponent(stat.icon) : null;
|
||||||
{stat?.icon && (
|
return (
|
||||||
<div className="mb-3 md:mb-4 group-hover:scale-110 transition-transform duration-300 flex items-center justify-center">
|
<div key={`stat-${index}-${stat.label || index}`} className="text-center group relative">
|
||||||
{stat.icon && (LucideIcons as any)[stat.icon] ? (
|
{stat?.icon && (
|
||||||
React.createElement((LucideIcons as any)[stat.icon], {
|
<div className="mb-3 md:mb-4 group-hover:scale-110 transition-transform duration-300 flex items-center justify-center">
|
||||||
className: 'w-8 h-8 md:w-10 md:h-10 text-[var(--luxury-gold)] drop-shadow-md'
|
{IconComponent ? (
|
||||||
})
|
React.createElement(IconComponent, {
|
||||||
) : (
|
className: 'w-8 h-8 md:w-10 md:h-10 text-[var(--luxury-gold)] drop-shadow-md'
|
||||||
<span className="text-3xl md:text-4xl">{stat.icon}</span>
|
})
|
||||||
)}
|
) : (
|
||||||
</div>
|
<span className="text-3xl md:text-4xl">✨</span>
|
||||||
)}
|
)}
|
||||||
{stat?.number && (
|
</div>
|
||||||
<div className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--luxury-gold)] mb-1 md:mb-2 font-serif tracking-tight">
|
)}
|
||||||
{stat.number}
|
{stat?.number && (
|
||||||
</div>
|
<div className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--luxury-gold)] mb-1 md:mb-2 font-serif tracking-tight">
|
||||||
)}
|
{stat.number}
|
||||||
{stat?.label && (
|
</div>
|
||||||
<div className="text-gray-300 text-xs sm:text-sm md:text-base font-light tracking-wider uppercase">
|
)}
|
||||||
{stat.label}
|
{stat?.label && (
|
||||||
</div>
|
<div className="text-gray-300 text-xs sm:text-sm md:text-base font-light tracking-wider uppercase">
|
||||||
)}
|
{stat.label}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
|
|||||||
import pageContentService from '../services/pageContentService';
|
import pageContentService from '../services/pageContentService';
|
||||||
import type { PageContent } from '../services/pageContentService';
|
import type { PageContent } from '../services/pageContentService';
|
||||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
import Loading from '../../../shared/components/Loading';
|
import Loading from '../../../shared/components/Loading';
|
||||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||||
|
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
const PrivacyPolicyPage: React.FC = () => {
|
const PrivacyPolicyPage: React.FC = () => {
|
||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPageContent = async () => {
|
const fetchPageContent = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -28,24 +35,26 @@ const PrivacyPolicyPage: React.FC = () => {
|
|||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = sanitizedContent;
|
tempDiv.innerHTML = sanitizedContent;
|
||||||
|
|
||||||
// Add color styles to elements that don't have them
|
// Get theme-aware colors
|
||||||
|
const isLightMode = theme.theme_layout_mode === 'light';
|
||||||
|
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||||
|
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||||
|
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||||
|
|
||||||
|
// Override inline colors to use theme-aware colors
|
||||||
const allElements = tempDiv.querySelectorAll('*');
|
const allElements = tempDiv.querySelectorAll('*');
|
||||||
allElements.forEach((el) => {
|
allElements.forEach((el) => {
|
||||||
const htmlEl = el as HTMLElement;
|
const htmlEl = el as HTMLElement;
|
||||||
const tagName = htmlEl.tagName.toLowerCase();
|
const tagName = htmlEl.tagName.toLowerCase();
|
||||||
const currentColor = htmlEl.style.color;
|
|
||||||
|
|
||||||
// Only add color if not already set
|
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
htmlEl.style.color = headingColor;
|
||||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
} else if (['strong', 'b'].includes(tagName)) {
|
||||||
htmlEl.style.color = '#ffffff';
|
htmlEl.style.color = accentColor;
|
||||||
} else if (['strong', 'b'].includes(tagName)) {
|
} else if (tagName === 'a') {
|
||||||
htmlEl.style.color = '#d4af37';
|
htmlEl.style.color = accentColor;
|
||||||
} else if (tagName === 'a') {
|
} else {
|
||||||
htmlEl.style.color = '#d4af37';
|
htmlEl.style.color = bodyColor;
|
||||||
} else {
|
|
||||||
htmlEl.style.color = '#d1d5db';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,7 +97,7 @@ const PrivacyPolicyPage: React.FC = () => {
|
|||||||
|
|
||||||
if (!pageContent) {
|
if (!pageContent) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -100,8 +109,8 @@ const PrivacyPolicyPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Shield className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
<Shield className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Privacy Policy</h1>
|
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Privacy Policy</h1>
|
||||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||||
@@ -115,7 +124,7 @@ const PrivacyPolicyPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -140,32 +149,34 @@ const PrivacyPolicyPage: React.FC = () => {
|
|||||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||||
<Shield className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
<Shield className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||||
|
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||||
{pageContent.title || 'Privacy Policy'}
|
{pageContent.title || 'Privacy Policy'}
|
||||||
</h1>
|
</h1>
|
||||||
{pageContent.subtitle && (
|
{pageContent.subtitle && (
|
||||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||||
{pageContent.subtitle}
|
{pageContent.subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||||
<div
|
<div
|
||||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
prose-headings:font-elegant prose-headings:font-semibold
|
||||||
|
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
prose-ul:font-light prose-ul:my-4
|
||||||
|
prose-li:mb-2 prose-li:ml-4
|
||||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
|
||||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
|
||||||
style={{ color: '#d1d5db' }}
|
|
||||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,7 +185,7 @@ const PrivacyPolicyPage: React.FC = () => {
|
|||||||
<div className="mt-8 space-y-4">
|
<div className="mt-8 space-y-4">
|
||||||
{settings.company_email && (
|
{settings.company_email && (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-gray-400 font-light">
|
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||||
For questions about this policy, contact us at{' '}
|
For questions about this policy, contact us at{' '}
|
||||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||||
{settings.company_email}
|
{settings.company_email}
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
|
|||||||
import pageContentService from '../services/pageContentService';
|
import pageContentService from '../services/pageContentService';
|
||||||
import type { PageContent } from '../services/pageContentService';
|
import type { PageContent } from '../services/pageContentService';
|
||||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
import Loading from '../../../shared/components/Loading';
|
import Loading from '../../../shared/components/Loading';
|
||||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||||
|
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
const RefundsPolicyPage: React.FC = () => {
|
const RefundsPolicyPage: React.FC = () => {
|
||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPageContent = async () => {
|
const fetchPageContent = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -28,24 +35,26 @@ const RefundsPolicyPage: React.FC = () => {
|
|||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = sanitizedContent;
|
tempDiv.innerHTML = sanitizedContent;
|
||||||
|
|
||||||
// Add color styles to elements that don't have them
|
// Get theme-aware colors
|
||||||
|
const isLightMode = theme.theme_layout_mode === 'light';
|
||||||
|
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||||
|
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||||
|
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||||
|
|
||||||
|
// Override inline colors to use theme-aware colors
|
||||||
const allElements = tempDiv.querySelectorAll('*');
|
const allElements = tempDiv.querySelectorAll('*');
|
||||||
allElements.forEach((el) => {
|
allElements.forEach((el) => {
|
||||||
const htmlEl = el as HTMLElement;
|
const htmlEl = el as HTMLElement;
|
||||||
const tagName = htmlEl.tagName.toLowerCase();
|
const tagName = htmlEl.tagName.toLowerCase();
|
||||||
const currentColor = htmlEl.style.color;
|
|
||||||
|
|
||||||
// Only add color if not already set
|
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
htmlEl.style.color = headingColor;
|
||||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
} else if (['strong', 'b'].includes(tagName)) {
|
||||||
htmlEl.style.color = '#ffffff';
|
htmlEl.style.color = accentColor;
|
||||||
} else if (['strong', 'b'].includes(tagName)) {
|
} else if (tagName === 'a') {
|
||||||
htmlEl.style.color = '#d4af37';
|
htmlEl.style.color = accentColor;
|
||||||
} else if (tagName === 'a') {
|
} else {
|
||||||
htmlEl.style.color = '#d4af37';
|
htmlEl.style.color = bodyColor;
|
||||||
} else {
|
|
||||||
htmlEl.style.color = '#d1d5db';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,7 +97,7 @@ const RefundsPolicyPage: React.FC = () => {
|
|||||||
|
|
||||||
if (!pageContent) {
|
if (!pageContent) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -100,8 +109,8 @@ const RefundsPolicyPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<RefreshCw className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
<RefreshCw className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Refunds Policy</h1>
|
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Refunds Policy</h1>
|
||||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||||
@@ -115,7 +124,7 @@ const RefundsPolicyPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -140,32 +149,34 @@ const RefundsPolicyPage: React.FC = () => {
|
|||||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||||
<RefreshCw className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
<RefreshCw className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||||
|
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||||
{pageContent.title || 'Refunds Policy'}
|
{pageContent.title || 'Refunds Policy'}
|
||||||
</h1>
|
</h1>
|
||||||
{pageContent.subtitle && (
|
{pageContent.subtitle && (
|
||||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||||
{pageContent.subtitle}
|
{pageContent.subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||||
<div
|
<div
|
||||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
prose-headings:font-elegant prose-headings:font-semibold
|
||||||
|
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
prose-ul:font-light prose-ul:my-4
|
||||||
|
prose-li:mb-2 prose-li:ml-4
|
||||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
|
||||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
|
||||||
style={{ color: '#d1d5db' }}
|
|
||||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,7 +184,7 @@ const RefundsPolicyPage: React.FC = () => {
|
|||||||
{/* Footer Note */}
|
{/* Footer Note */}
|
||||||
{settings.company_email && (
|
{settings.company_email && (
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
<p className="text-sm text-gray-400 font-light">
|
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||||
For refund inquiries, contact us at{' '}
|
For refund inquiries, contact us at{' '}
|
||||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||||
{settings.company_email}
|
{settings.company_email}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import serviceService from '../../hotel_services/services/serviceService';
|
|||||||
import Loading from '../../../shared/components/Loading';
|
import Loading from '../../../shared/components/Loading';
|
||||||
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
|
import { createSanitizedHtml } from '../../../shared/utils/htmlSanitizer';
|
||||||
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
|
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
interface ServiceSection {
|
interface ServiceSection {
|
||||||
type: 'hero' | 'text' | 'image' | 'gallery' | 'quote' | 'features' | 'cta' | 'video';
|
type: 'hero' | 'text' | 'image' | 'gallery' | 'quote' | 'features' | 'cta' | 'video';
|
||||||
@@ -47,12 +49,17 @@ interface ServiceDetail {
|
|||||||
const ServiceDetailPage: React.FC = () => {
|
const ServiceDetailPage: React.FC = () => {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { theme } = useTheme();
|
||||||
const { formatCurrency } = useFormatCurrency();
|
const { formatCurrency } = useFormatCurrency();
|
||||||
const [service, setService] = useState<ServiceDetail | null>(null);
|
const [service, setService] = useState<ServiceDetail | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [relatedServices, setRelatedServices] = useState<ServiceDetail[]>([]);
|
const [relatedServices, setRelatedServices] = useState<ServiceDetail[]>([]);
|
||||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||||
|
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (slug) {
|
if (slug) {
|
||||||
fetchService();
|
fetchService();
|
||||||
@@ -288,9 +295,9 @@ const ServiceDetailPage: React.FC = () => {
|
|||||||
|
|
||||||
if (!service) {
|
if (!service) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black text-white flex items-center justify-center">
|
<div className={`min-h-screen ${bgClasses} flex items-center justify-center`}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold mb-4">Service not found</h1>
|
<h1 className={`text-2xl font-bold mb-4 ${textClasses.primary}`}>Service not found</h1>
|
||||||
<Link to="/services" className="text-[var(--luxury-gold)] hover:underline">
|
<Link to="/services" className="text-[var(--luxury-gold)] hover:underline">
|
||||||
Back to Services
|
Back to Services
|
||||||
</Link>
|
</Link>
|
||||||
@@ -300,7 +307,7 @@ const ServiceDetailPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||||
{/* Hero Section with Featured Image - Enhanced Luxury */}
|
{/* Hero Section with Featured Image - Enhanced Luxury */}
|
||||||
{service.image && (
|
{service.image && (
|
||||||
<div className="relative w-full h-[60vh] min-h-[500px] max-h-[800px] overflow-hidden">
|
<div className="relative w-full h-[60vh] min-h-[500px] max-h-[800px] overflow-hidden">
|
||||||
@@ -338,11 +345,11 @@ const ServiceDetailPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-serif font-bold text-white mb-6 leading-tight drop-shadow-2xl">
|
<h1 className={`text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-serif font-bold ${textClasses.primary} mb-6 leading-tight drop-shadow-2xl`}>
|
||||||
{service.title}
|
{service.title}
|
||||||
</h1>
|
</h1>
|
||||||
{service.description && (
|
{service.description && (
|
||||||
<p className="text-xl sm:text-2xl lg:text-3xl text-gray-200 font-light leading-relaxed max-w-4xl mb-8 drop-shadow-lg">
|
<p className={`text-xl sm:text-2xl lg:text-3xl ${textClasses.secondary} font-light leading-relaxed max-w-4xl mb-8 drop-shadow-lg`}>
|
||||||
{service.description}
|
{service.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -352,7 +359,7 @@ const ServiceDetailPage: React.FC = () => {
|
|||||||
{formatCurrency(service.price)}
|
{formatCurrency(service.price)}
|
||||||
</span>
|
</span>
|
||||||
{service.unit && (
|
{service.unit && (
|
||||||
<span className="text-xl sm:text-2xl text-gray-300 font-light">
|
<span className={`text-xl sm:text-2xl ${textClasses.secondary} font-light`}>
|
||||||
/ {service.unit}
|
/ {service.unit}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -375,7 +382,7 @@ const ServiceDetailPage: React.FC = () => {
|
|||||||
{!service.image && (
|
{!service.image && (
|
||||||
<Link
|
<Link
|
||||||
to="/services"
|
to="/services"
|
||||||
className="inline-flex items-center gap-3 px-6 py-3 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] border-2 border-[var(--luxury-gold)]/20 rounded-xl text-gray-300 hover:text-[var(--luxury-gold)] hover:border-[var(--luxury-gold)]/50 transition-all duration-300 mb-12 group"
|
className={`inline-flex items-center gap-3 px-6 py-3 ${cardClasses} border-2 border-[var(--luxury-gold)]/20 rounded-xl ${textClasses.secondary} hover:text-[var(--luxury-gold)] hover:border-[var(--luxury-gold)]/50 transition-all duration-300 mb-12 group`}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
|
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
|
||||||
<span className="font-medium">Back to Services</span>
|
<span className="font-medium">Back to Services</span>
|
||||||
@@ -422,7 +429,7 @@ const ServiceDetailPage: React.FC = () => {
|
|||||||
{formatCurrency(service.price)}
|
{formatCurrency(service.price)}
|
||||||
</span>
|
</span>
|
||||||
{service.unit && (
|
{service.unit && (
|
||||||
<span className="text-xl text-gray-400 font-light">
|
<span className={`text-xl ${textClasses.muted} font-light`}>
|
||||||
/ {service.unit}
|
/ {service.unit}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -513,12 +520,12 @@ const ServiceDetailPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="relative px-8 py-20 md:px-16 md:py-32 text-center">
|
<div className="relative px-8 py-20 md:px-16 md:py-32 text-center">
|
||||||
{section.title && (
|
{section.title && (
|
||||||
<h2 className="text-5xl md:text-6xl lg:text-7xl font-serif font-bold text-white mb-8 leading-tight drop-shadow-2xl">
|
<h2 className={`text-5xl md:text-6xl lg:text-7xl font-serif font-bold ${textClasses.primary} mb-8 leading-tight drop-shadow-2xl`}>
|
||||||
{section.title}
|
{section.title}
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
{section.content && (
|
{section.content && (
|
||||||
<p className="text-2xl md:text-3xl text-gray-200 font-light leading-relaxed max-w-4xl mx-auto drop-shadow-lg">
|
<p className={`text-2xl md:text-3xl ${textClasses.secondary} font-light leading-relaxed max-w-4xl mx-auto drop-shadow-lg`}>
|
||||||
{section.content}
|
{section.content}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -529,17 +536,17 @@ const ServiceDetailPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* Text Section */}
|
{/* Text Section */}
|
||||||
{section.type === 'text' && (
|
{section.type === 'text' && (
|
||||||
<div className={`bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 md:p-12 shadow-xl ${
|
<div className={`${cardClasses} rounded-3xl border-2 border-[var(--luxury-gold)]/20 p-8 md:p-12 shadow-xl ${
|
||||||
section.alignment === 'center' ? 'text-center' : section.alignment === 'right' ? 'text-right' : 'text-left'
|
section.alignment === 'center' ? 'text-center' : section.alignment === 'right' ? 'text-right' : 'text-left'
|
||||||
}`}>
|
}`}>
|
||||||
{section.title && (
|
{section.title && (
|
||||||
<h3 className="text-3xl md:text-4xl font-serif font-bold text-white mb-6">
|
<h3 className={`text-3xl md:text-4xl font-serif font-bold ${textClasses.primary} mb-6`}>
|
||||||
{section.title}
|
{section.title}
|
||||||
</h3>
|
</h3>
|
||||||
)}
|
)}
|
||||||
{section.content && (
|
{section.content && (
|
||||||
<div
|
<div
|
||||||
className="text-gray-300 font-light leading-relaxed text-lg"
|
className={`${textClasses.secondary} font-light leading-relaxed text-lg`}
|
||||||
dangerouslySetInnerHTML={createSanitizedHtml(section.content)}
|
dangerouslySetInnerHTML={createSanitizedHtml(section.content)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -551,8 +558,8 @@ const ServiceDetailPage: React.FC = () => {
|
|||||||
<div className="rounded-3xl overflow-hidden border-2 border-[var(--luxury-gold)]/20 shadow-2xl">
|
<div className="rounded-3xl overflow-hidden border-2 border-[var(--luxury-gold)]/20 shadow-2xl">
|
||||||
<img src={section.image} alt={section.title || 'Service image'} className="w-full h-auto" />
|
<img src={section.image} alt={section.title || 'Service image'} className="w-full h-auto" />
|
||||||
{section.title && (
|
{section.title && (
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] px-6 py-4 border-t border-[var(--luxury-gold)]/20">
|
<div className={`${cardClasses} px-6 py-4 border-t border-[var(--luxury-gold)]/20`}>
|
||||||
<p className="text-gray-400 text-sm font-light italic text-center">{section.title}</p>
|
<p className={`${textClasses.muted} text-sm font-light italic text-center`}>{section.title}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -666,7 +673,7 @@ const ServiceDetailPage: React.FC = () => {
|
|||||||
{/* Related Services */}
|
{/* Related Services */}
|
||||||
{relatedServices.length > 0 && (
|
{relatedServices.length > 0 && (
|
||||||
<div className="mt-16 pt-12 border-t border-[var(--luxury-gold)]/20">
|
<div className="mt-16 pt-12 border-t border-[var(--luxury-gold)]/20">
|
||||||
<h2 className="text-2xl sm:text-3xl font-bold text-white mb-8">Related Services</h2>
|
<h2 className={`text-2xl sm:text-3xl font-bold ${textClasses.primary} mb-8`}>Related Services</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
{relatedServices.map((relatedService) => (
|
{relatedServices.map((relatedService) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -684,11 +691,11 @@ const ServiceDetailPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<h3 className="text-lg font-bold text-white mb-2 group-hover:text-[var(--luxury-gold)] transition-colors line-clamp-2">
|
<h3 className={`text-lg font-bold ${textClasses.primary} mb-2 group-hover:text-[var(--luxury-gold)] transition-colors line-clamp-2`}>
|
||||||
{relatedService.title}
|
{relatedService.title}
|
||||||
</h3>
|
</h3>
|
||||||
{relatedService.description && (
|
{relatedService.description && (
|
||||||
<p className="text-gray-400 text-sm line-clamp-2 font-light">
|
<p className={`${textClasses.muted} text-sm line-clamp-2 font-light`}>
|
||||||
{relatedService.description}
|
{relatedService.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,8 +6,29 @@ import pageContentService, { PageContent } from '../services/pageContentService'
|
|||||||
import serviceService, { Service } from '../../hotel_services/services/serviceService';
|
import serviceService, { Service } from '../../hotel_services/services/serviceService';
|
||||||
import Loading from '../../../shared/components/Loading';
|
import Loading from '../../../shared/components/Loading';
|
||||||
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
|
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
|
// Helper function to get icon component from icon name (handles both PascalCase and lowercase)
|
||||||
|
const getIconComponent = (iconName?: string, fallback: any = Award) => {
|
||||||
|
if (!iconName) return fallback;
|
||||||
|
|
||||||
|
// Try direct match first (for PascalCase names)
|
||||||
|
if ((LucideIcons as any)[iconName]) {
|
||||||
|
return (LucideIcons as any)[iconName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to PascalCase (capitalize first letter)
|
||||||
|
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1).toLowerCase();
|
||||||
|
if ((LucideIcons as any)[pascalCaseName]) {
|
||||||
|
return (LucideIcons as any)[pascalCaseName];
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
const ServicesPage: React.FC = () => {
|
const ServicesPage: React.FC = () => {
|
||||||
|
const { theme } = useTheme();
|
||||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||||
const [hotelServices, setHotelServices] = useState<Service[]>([]);
|
const [hotelServices, setHotelServices] = useState<Service[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -156,10 +177,16 @@ const ServicesPage: React.FC = () => {
|
|||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8 overflow-hidden relative">
|
<div className={`w-full ${heroBgClasses} pt-6 sm:pt-7 md:pt-8 overflow-hidden relative`}>
|
||||||
{/* Background Effects */}
|
{/* Background Effects */}
|
||||||
<div className="absolute inset-0 opacity-10">
|
<div className="absolute inset-0 opacity-10">
|
||||||
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[var(--luxury-gold)] rounded-full blur-3xl"></div>
|
||||||
@@ -172,18 +199,20 @@ const ServicesPage: React.FC = () => {
|
|||||||
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
|
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
|
<div className="absolute inset-0 bg-gradient-to-br from-[var(--luxury-gold)] via-[var(--luxury-gold-light)] to-[var(--luxury-gold-dark)] rounded-xl blur-lg opacity-40 group-hover:opacity-60 transition-opacity duration-500"></div>
|
||||||
<div className="relative p-2 sm:p-2.5 md:p-3 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300">
|
<div className={`relative p-2 sm:p-2.5 md:p-3 ${cardClasses} rounded-lg border-2 border-[var(--luxury-gold)]/40 backdrop-blur-sm shadow-xl shadow-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/60 transition-all duration-300`}>
|
||||||
<Award className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg" />
|
<Award className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[var(--luxury-gold)] drop-shadow-lg" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
|
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
|
||||||
<span className="bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
<span className={`${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||||
|
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||||
{pageContent?.luxury_services_section_title || 'Our Services'}
|
{pageContent?.luxury_services_section_title || 'Our Services'}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
|
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mx-auto mb-2 sm:mb-3"></div>
|
||||||
<p className="text-sm sm:text-base md:text-lg text-gray-300 font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4">
|
<p className={`text-sm sm:text-base md:text-lg ${textClasses.secondary} font-light leading-relaxed max-w-xl mx-auto tracking-wide px-2 sm:px-4`}>
|
||||||
{pageContent?.luxury_services_section_subtitle || 'Discover our premium services designed to enhance your stay'}
|
{pageContent?.luxury_services_section_subtitle || 'Discover our premium services designed to enhance your stay'}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 flex items-center justify-center gap-2 text-[var(--luxury-gold)]/60">
|
<div className="mt-4 flex items-center justify-center gap-2 text-[var(--luxury-gold)]/60">
|
||||||
@@ -211,7 +240,7 @@ const ServicesPage: React.FC = () => {
|
|||||||
placeholder="Search services..."
|
placeholder="Search services..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="w-full pl-14 pr-5 py-4 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] border border-[var(--luxury-gold)]/20 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light"
|
className={`w-full pl-14 pr-5 py-4 ${inputClasses} border border-[var(--luxury-gold)]/20 rounded-xl placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/50 focus:border-[var(--luxury-gold)]/50 transition-all duration-300 backdrop-blur-sm font-light`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -221,13 +250,13 @@ const ServicesPage: React.FC = () => {
|
|||||||
{/* Categories Filter - Top Center */}
|
{/* Categories Filter - Top Center */}
|
||||||
{allCategories.length > 0 && (
|
{allCategories.length > 0 && (
|
||||||
<div className="mb-12 flex justify-center">
|
<div className="mb-12 flex justify-center">
|
||||||
<div className="inline-flex flex-wrap items-center gap-3 bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-2xl border-2 border-[var(--luxury-gold)]/20 p-4 backdrop-blur-xl shadow-2xl">
|
<div className={`inline-flex flex-wrap items-center gap-3 ${cardClasses} rounded-2xl border-2 border-[var(--luxury-gold)]/20 p-4 backdrop-blur-xl shadow-2xl`}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedCategory(null)}
|
onClick={() => setSelectedCategory(null)}
|
||||||
className={`group relative px-6 py-3 rounded-xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
className={`group relative px-6 py-3 rounded-xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
||||||
selectedCategory === null
|
selectedCategory === null
|
||||||
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
||||||
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)]'
|
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="relative z-10 flex items-center gap-2">
|
<span className="relative z-10 flex items-center gap-2">
|
||||||
@@ -242,7 +271,7 @@ const ServicesPage: React.FC = () => {
|
|||||||
className={`group relative px-6 py-3 rounded-xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
className={`group relative px-6 py-3 rounded-xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
||||||
selectedCategory === category
|
selectedCategory === category
|
||||||
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-[#0f0f0f] shadow-lg shadow-[var(--luxury-gold)]/40'
|
||||||
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)]'
|
: `${cardClasses} ${textClasses.secondary} border-2 border-[var(--luxury-gold)]/20 hover:border-[var(--luxury-gold)]/50 hover:text-[var(--luxury-gold)] ${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-100' : 'hover:bg-[#1a1a1a]'}`
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="relative z-10 flex items-center gap-2">
|
<span className="relative z-10 flex items-center gap-2">
|
||||||
@@ -259,7 +288,7 @@ const ServicesPage: React.FC = () => {
|
|||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
{/* Results Count */}
|
{/* Results Count */}
|
||||||
{!loading && filteredServices.length > 0 && (
|
{!loading && filteredServices.length > 0 && (
|
||||||
<div className="mb-8 text-gray-400 font-light text-sm text-center">
|
<div className={`mb-8 ${textClasses.muted} font-light text-sm text-center`}>
|
||||||
Showing {filteredServices.length} of {allServices.length} services
|
Showing {filteredServices.length} of {allServices.length} services
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -270,8 +299,8 @@ const ServicesPage: React.FC = () => {
|
|||||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--luxury-gold)]/10 mb-6">
|
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--luxury-gold)]/10 mb-6">
|
||||||
<Award className="w-10 h-10 text-[var(--luxury-gold)]" />
|
<Award className="w-10 h-10 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-400 text-xl font-light">No services found</p>
|
<p className={`${textClasses.muted} text-xl font-light`}>No services found</p>
|
||||||
<p className="text-gray-500 text-sm mt-2">Try adjusting your search or filters</p>
|
<p className={`${textClasses.muted} text-sm mt-2`}>Try adjusting your search or filters</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -288,7 +317,7 @@ const ServicesPage: React.FC = () => {
|
|||||||
<Link
|
<Link
|
||||||
key={service.id}
|
key={service.id}
|
||||||
to={`/services/${serviceSlug}`}
|
to={`/services/${serviceSlug}`}
|
||||||
className="group relative bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3 block"
|
className={`group relative ${cardClasses} rounded-3xl border-2 border-[var(--luxury-gold)]/20 overflow-hidden hover:border-[var(--luxury-gold)]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[var(--luxury-gold)]/30 hover:-translate-y-3 block`}
|
||||||
style={{ animationDelay: `${index * 50}ms` }}
|
style={{ animationDelay: `${index * 50}ms` }}
|
||||||
>
|
>
|
||||||
{/* Premium Glow Effects */}
|
{/* Premium Glow Effects */}
|
||||||
@@ -320,7 +349,7 @@ const ServicesPage: React.FC = () => {
|
|||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-48 sm:h-56 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] flex items-center justify-center p-8">
|
<div className={`h-48 sm:h-56 ${cardClasses} flex items-center justify-center p-8`}>
|
||||||
{service.icon && (LucideIcons as any)[service.icon] ? (
|
{service.icon && (LucideIcons as any)[service.icon] ? (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div>
|
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div>
|
||||||
@@ -331,7 +360,12 @@ const ServicesPage: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div>
|
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 rounded-full blur-2xl"></div>
|
||||||
<Award className="w-16 h-16 sm:w-20 sm:h-20 text-[var(--luxury-gold)] relative z-10 drop-shadow-lg" />
|
{React.createElement(
|
||||||
|
getIconComponent(pageContent?.services_fallback_icon, Award),
|
||||||
|
{
|
||||||
|
className: 'w-16 h-16 sm:w-20 sm:h-20 text-[var(--luxury-gold)] relative z-10 drop-shadow-lg'
|
||||||
|
}
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -345,11 +379,11 @@ const ServicesPage: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h2 className="text-2xl sm:text-3xl font-serif font-bold text-white mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight">
|
<h2 className={`text-2xl sm:text-3xl font-serif font-bold ${textClasses.primary} mb-4 group-hover:text-[var(--luxury-gold)] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight`}>
|
||||||
{service.title}
|
{service.title}
|
||||||
</h2>
|
</h2>
|
||||||
{service.description && (
|
{service.description && (
|
||||||
<p className="text-gray-300 text-base mb-6 line-clamp-3 font-light leading-relaxed">
|
<p className={`${textClasses.secondary} text-base mb-6 line-clamp-3 font-light leading-relaxed`}>
|
||||||
{service.description}
|
{service.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -360,7 +394,7 @@ const ServicesPage: React.FC = () => {
|
|||||||
{formatCurrency(service.price)}
|
{formatCurrency(service.price)}
|
||||||
</span>
|
</span>
|
||||||
{service.unit && (
|
{service.unit && (
|
||||||
<span className="text-sm text-gray-400 font-light">
|
<span className={`text-sm ${textClasses.muted} font-light`}>
|
||||||
/ {service.unit}
|
/ {service.unit}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import { Link } from 'react-router-dom';
|
|||||||
import pageContentService from '../services/pageContentService';
|
import pageContentService from '../services/pageContentService';
|
||||||
import type { PageContent } from '../services/pageContentService';
|
import type { PageContent } from '../services/pageContentService';
|
||||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
import Loading from '../../../shared/components/Loading';
|
import Loading from '../../../shared/components/Loading';
|
||||||
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
import { createSanitizedHtml, sanitizeHtml } from '../../../shared/utils/htmlSanitizer';
|
||||||
|
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
const TermsPage: React.FC = () => {
|
const TermsPage: React.FC = () => {
|
||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPageContent = async () => {
|
const fetchPageContent = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -28,24 +35,26 @@ const TermsPage: React.FC = () => {
|
|||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = sanitizedContent;
|
tempDiv.innerHTML = sanitizedContent;
|
||||||
|
|
||||||
// Add color styles to elements that don't have them
|
// Get theme-aware colors
|
||||||
|
const isLightMode = theme.theme_layout_mode === 'light';
|
||||||
|
const headingColor = isLightMode ? '#111827' : '#ffffff';
|
||||||
|
const bodyColor = isLightMode ? '#111827' : '#d1d5db';
|
||||||
|
const accentColor = '#d4af37'; // Gold color for links and strong
|
||||||
|
|
||||||
|
// Override inline colors to use theme-aware colors
|
||||||
const allElements = tempDiv.querySelectorAll('*');
|
const allElements = tempDiv.querySelectorAll('*');
|
||||||
allElements.forEach((el) => {
|
allElements.forEach((el) => {
|
||||||
const htmlEl = el as HTMLElement;
|
const htmlEl = el as HTMLElement;
|
||||||
const tagName = htmlEl.tagName.toLowerCase();
|
const tagName = htmlEl.tagName.toLowerCase();
|
||||||
const currentColor = htmlEl.style.color;
|
|
||||||
|
|
||||||
// Only add color if not already set
|
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
htmlEl.style.color = headingColor;
|
||||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
} else if (['strong', 'b'].includes(tagName)) {
|
||||||
htmlEl.style.color = '#ffffff';
|
htmlEl.style.color = accentColor;
|
||||||
} else if (['strong', 'b'].includes(tagName)) {
|
} else if (tagName === 'a') {
|
||||||
htmlEl.style.color = '#d4af37';
|
htmlEl.style.color = accentColor;
|
||||||
} else if (tagName === 'a') {
|
} else {
|
||||||
htmlEl.style.color = '#d4af37';
|
htmlEl.style.color = bodyColor;
|
||||||
} else {
|
|
||||||
htmlEl.style.color = '#d1d5db';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,7 +97,7 @@ const TermsPage: React.FC = () => {
|
|||||||
|
|
||||||
if (!pageContent) {
|
if (!pageContent) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6 flex items-center justify-center`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -100,8 +109,8 @@ const TermsPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Scale className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
<Scale className="w-16 h-16 text-[var(--luxury-gold)]/50 mx-auto mb-4" />
|
||||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Terms & Conditions</h1>
|
<h1 className={`text-2xl font-elegant font-bold ${textClasses.primary} mb-2`}>Terms & Conditions</h1>
|
||||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
<p className={textClasses.muted}>This page is currently unavailable.</p>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
className="inline-flex items-center gap-2 text-[var(--luxury-gold)]/80 hover:text-[var(--luxury-gold)] mt-6 transition-all duration-300"
|
||||||
@@ -115,7 +124,7 @@ const TermsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
<div className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -140,32 +149,34 @@ const TermsPage: React.FC = () => {
|
|||||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/10 rounded-full border border-[var(--luxury-gold)]/30">
|
||||||
<Scale className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
<Scale className="w-8 h-8 sm:w-10 sm:h-10 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white bg-clip-text text-transparent">
|
<h1 className={`text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold ${textClasses.primary} mb-3 sm:mb-4 tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||||
|
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'} bg-clip-text text-transparent`}>
|
||||||
{pageContent.title || 'Terms & Conditions'}
|
{pageContent.title || 'Terms & Conditions'}
|
||||||
</h1>
|
</h1>
|
||||||
{pageContent.subtitle && (
|
{pageContent.subtitle && (
|
||||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
<p className={`text-base sm:text-lg ${textClasses.secondary} font-light tracking-wide`}>
|
||||||
{pageContent.subtitle}
|
{pageContent.subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12">
|
<div className={`${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20 backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5 p-6 sm:p-8 lg:p-12`}>
|
||||||
<div
|
<div
|
||||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
className={`prose ${theme.theme_layout_mode === 'light' ? 'prose-slate' : 'prose-invert'} prose-lg max-w-none
|
||||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
prose-headings:font-elegant prose-headings:font-semibold
|
||||||
|
${theme.theme_layout_mode === 'light' ? 'prose-headings:text-gray-900' : 'prose-headings:text-white'}
|
||||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[var(--luxury-gold)]/20 prose-h2:pb-2
|
||||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
${theme.theme_layout_mode === 'light' ? 'prose-p:text-gray-900 prose-ul:text-gray-900 prose-ol:text-gray-900 prose-li:text-gray-900' : 'prose-p:text-gray-300 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:text-gray-300'}
|
||||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
prose-ul:font-light prose-ul:my-4
|
||||||
|
prose-li:mb-2 prose-li:ml-4
|
||||||
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
prose-strong:text-[var(--luxury-gold)] prose-strong:font-medium
|
||||||
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline
|
prose-a:text-[var(--luxury-gold)] prose-a:no-underline hover:prose-a:underline`}
|
||||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
style={{ color: theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db' }}
|
||||||
[&_strong]:text-[var(--luxury-gold)] [&_b]:text-[var(--luxury-gold)] [&_a]:text-[var(--luxury-gold)]"
|
|
||||||
style={{ color: '#d1d5db' }}
|
|
||||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
pageContent.content || pageContent.description || `<p style="color: ${theme.theme_layout_mode === 'light' ? '#111827' : '#d1d5db'};">No content available.</p>`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,7 +184,7 @@ const TermsPage: React.FC = () => {
|
|||||||
{/* Footer Note */}
|
{/* Footer Note */}
|
||||||
{settings.company_email && (
|
{settings.company_email && (
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
<p className="text-sm text-gray-400 font-light">
|
<p className={`text-sm ${textClasses.muted} font-light`}>
|
||||||
For questions about these terms, contact us at{' '}
|
For questions about these terms, contact us at{' '}
|
||||||
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
<a href={`mailto:${settings.company_email}`} className="text-[var(--luxury-gold)] hover:underline">
|
||||||
{settings.company_email}
|
{settings.company_email}
|
||||||
|
|||||||
@@ -21,6 +21,18 @@ export interface PageContent {
|
|||||||
email?: string;
|
email?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
};
|
};
|
||||||
|
contact_icons?: {
|
||||||
|
hero?: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
location?: string;
|
||||||
|
name_field?: string;
|
||||||
|
email_field?: string;
|
||||||
|
phone_field?: string;
|
||||||
|
subject_field?: string;
|
||||||
|
message_field?: string;
|
||||||
|
submit_button?: string;
|
||||||
|
};
|
||||||
map_url?: string;
|
map_url?: string;
|
||||||
social_links?: {
|
social_links?: {
|
||||||
facebook?: string;
|
facebook?: string;
|
||||||
@@ -46,6 +58,13 @@ export interface PageContent {
|
|||||||
features_section_title?: string;
|
features_section_title?: string;
|
||||||
features_section_subtitle?: string;
|
features_section_subtitle?: string;
|
||||||
about_hero_image?: string;
|
about_hero_image?: string;
|
||||||
|
about_hero_icon?: string;
|
||||||
|
about_contact_icons?: {
|
||||||
|
location?: string;
|
||||||
|
phone?: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
about_learn_more_icon?: string;
|
||||||
mission?: string;
|
mission?: string;
|
||||||
vision?: string;
|
vision?: string;
|
||||||
team?: Array<{ name: string; role: string; image?: string; bio?: string; social_links?: { linkedin?: string; twitter?: string; email?: string } }>;
|
team?: Array<{ name: string; role: string; image?: string; bio?: string; social_links?: { linkedin?: string; twitter?: string; email?: string } }>;
|
||||||
@@ -88,6 +107,7 @@ export interface PageContent {
|
|||||||
services_section_button_text?: string;
|
services_section_button_text?: string;
|
||||||
services_section_button_link?: string;
|
services_section_button_link?: string;
|
||||||
services_section_limit?: number;
|
services_section_limit?: number;
|
||||||
|
services_fallback_icon?: string;
|
||||||
luxury_services?: Array<{
|
luxury_services?: Array<{
|
||||||
icon?: string;
|
icon?: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -204,6 +224,18 @@ export interface UpdatePageContentData {
|
|||||||
email?: string;
|
email?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
};
|
};
|
||||||
|
contact_icons?: {
|
||||||
|
hero?: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
location?: string;
|
||||||
|
name_field?: string;
|
||||||
|
email_field?: string;
|
||||||
|
phone_field?: string;
|
||||||
|
subject_field?: string;
|
||||||
|
message_field?: string;
|
||||||
|
submit_button?: string;
|
||||||
|
};
|
||||||
map_url?: string;
|
map_url?: string;
|
||||||
social_links?: {
|
social_links?: {
|
||||||
facebook?: string;
|
facebook?: string;
|
||||||
@@ -229,6 +261,13 @@ export interface UpdatePageContentData {
|
|||||||
features_section_title?: string;
|
features_section_title?: string;
|
||||||
features_section_subtitle?: string;
|
features_section_subtitle?: string;
|
||||||
about_hero_image?: string;
|
about_hero_image?: string;
|
||||||
|
about_hero_icon?: string;
|
||||||
|
about_contact_icons?: {
|
||||||
|
location?: string;
|
||||||
|
phone?: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
about_learn_more_icon?: string;
|
||||||
mission?: string;
|
mission?: string;
|
||||||
vision?: string;
|
vision?: string;
|
||||||
team?: Array<{ name: string; role: string; image?: string; bio?: string; social_links?: { linkedin?: string; twitter?: string; email?: string } }>;
|
team?: Array<{ name: string; role: string; image?: string; bio?: string; social_links?: { linkedin?: string; twitter?: string; email?: string } }>;
|
||||||
@@ -271,6 +310,7 @@ export interface UpdatePageContentData {
|
|||||||
services_section_button_text?: string;
|
services_section_button_text?: string;
|
||||||
services_section_button_link?: string;
|
services_section_button_link?: string;
|
||||||
services_section_limit?: number;
|
services_section_limit?: number;
|
||||||
|
services_fallback_icon?: string;
|
||||||
luxury_services?: Array<{
|
luxury_services?: Array<{
|
||||||
icon?: string;
|
icon?: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import useAuthStore from '../../../store/useAuthStore';
|
|||||||
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import ConfirmationDialog from '../../../shared/components/ConfirmationDialog';
|
import ConfirmationDialog from '../../../shared/components/ConfirmationDialog';
|
||||||
|
import { formatWorkingHours } from '../../../shared/utils/format';
|
||||||
|
|
||||||
interface ChatWidgetProps {
|
interface ChatWidgetProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
@@ -405,7 +406,9 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
|
|||||||
{!isWithinBusinessHours ? (
|
{!isWithinBusinessHours ? (
|
||||||
<p className="text-xs text-slate-700/80 font-light flex items-center gap-1">
|
<p className="text-xs text-slate-700/80 font-light flex items-center gap-1">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
Chat available 9 AM - 5 PM
|
Chat available {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined
|
||||||
|
? formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)
|
||||||
|
: '9:00 AM - 5:00 PM'}
|
||||||
</p>
|
</p>
|
||||||
) : chat?.status === 'pending' ? (
|
) : chat?.status === 'pending' ? (
|
||||||
<p className="text-xs text-slate-700/80 font-light">Waiting for staff...</p>
|
<p className="text-xs text-slate-700/80 font-light">Waiting for staff...</p>
|
||||||
@@ -459,7 +462,9 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-slate-900 mb-1">Chat Hours</p>
|
<p className="text-sm font-semibold text-slate-900 mb-1">Chat Hours</p>
|
||||||
<p className="text-xs text-slate-600 font-light">
|
<p className="text-xs text-slate-600 font-light">
|
||||||
Our chat support is available Monday to Friday, {settings.chat_working_hours_start || 9}:00 AM - {settings.chat_working_hours_end || 17}:00 PM.
|
Our chat support is available Monday to Friday, {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined
|
||||||
|
? formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)
|
||||||
|
: '9:00 AM - 5:00 PM'}.
|
||||||
Please leave your inquiry below and we'll get back to you as soon as possible.
|
Please leave your inquiry below and we'll get back to you as soon as possible.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -585,6 +590,18 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
|
|||||||
{}
|
{}
|
||||||
{!showVisitorForm && isWithinBusinessHours && (
|
{!showVisitorForm && isWithinBusinessHours && (
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-slate-50/50 to-white">
|
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-slate-50/50 to-white">
|
||||||
|
{/* Working Hours Info Banner */}
|
||||||
|
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
|
||||||
|
<div className="mb-4 p-3 bg-gradient-to-r from-[var(--luxury-gold)]/5 to-[var(--luxury-gold-dark)]/5 rounded-lg border border-[var(--luxury-gold)]/20">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="w-4 h-4 text-[var(--luxury-gold)] flex-shrink-0" />
|
||||||
|
<p className="text-xs text-slate-600 font-light">
|
||||||
|
<span className="font-medium text-slate-700">Chat Support Hours:</span>{' '}
|
||||||
|
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{loading && !chat ? (
|
{loading && !chat ? (
|
||||||
<div className="text-center text-slate-500 py-8 font-light">
|
<div className="text-center text-slate-500 py-8 font-light">
|
||||||
Starting chat...
|
Starting chat...
|
||||||
|
|||||||
@@ -30,14 +30,13 @@ interface CurrencyProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CurrencyProvider: React.FC<CurrencyProviderProps> = ({ children }) => {
|
export const CurrencyProvider: React.FC<CurrencyProviderProps> = ({ children }) => {
|
||||||
const [currency, setCurrencyState] = useState<string>(CURRENCY.VND);
|
const [currency, setCurrencyState] = useState<string>(CURRENCY.USD);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
const supportedCurrencies = [
|
const supportedCurrencies = [
|
||||||
CURRENCY.VND,
|
|
||||||
CURRENCY.USD,
|
CURRENCY.USD,
|
||||||
CURRENCY.EUR,
|
CURRENCY.EUR,
|
||||||
'GBP',
|
CURRENCY.GBP,
|
||||||
'JPY',
|
'JPY',
|
||||||
'CNY',
|
'CNY',
|
||||||
'KRW',
|
'KRW',
|
||||||
@@ -59,14 +58,14 @@ export const CurrencyProvider: React.FC<CurrencyProviderProps> = ({ children })
|
|||||||
localStorage.setItem('currency', platformCurrency);
|
localStorage.setItem('currency', platformCurrency);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
setCurrencyState(CURRENCY.VND);
|
setCurrencyState(CURRENCY.USD);
|
||||||
localStorage.setItem('currency', CURRENCY.VND);
|
localStorage.setItem('currency', CURRENCY.USD);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading platform currency:', error);
|
console.error('Error loading platform currency:', error);
|
||||||
|
|
||||||
setCurrencyState(CURRENCY.VND);
|
setCurrencyState(CURRENCY.USD);
|
||||||
localStorage.setItem('currency', CURRENCY.VND);
|
localStorage.setItem('currency', CURRENCY.USD);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -81,7 +80,7 @@ export const CurrencyProvider: React.FC<CurrencyProviderProps> = ({ children })
|
|||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.dispatchEvent(new CustomEvent('currencyChanged', {
|
window.dispatchEvent(new CustomEvent('currencyChanged', {
|
||||||
detail: { currency: localStorage.getItem('currency') || CURRENCY.VND }
|
detail: { currency: localStorage.getItem('currency') || CURRENCY.USD }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import Recaptcha from '../../../shared/components/Recaptcha';
|
|||||||
import { recaptchaService } from '../../system/services/systemSettingsService';
|
import { recaptchaService } from '../../system/services/systemSettingsService';
|
||||||
import { useAntibotForm } from '../../auth/hooks/useAntibotForm';
|
import { useAntibotForm } from '../../auth/hooks/useAntibotForm';
|
||||||
import HoneypotField from '../../../shared/components/HoneypotField';
|
import HoneypotField from '../../../shared/components/HoneypotField';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
|
import { getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
interface ReviewSectionProps {
|
interface ReviewSectionProps {
|
||||||
roomId: number;
|
roomId: number;
|
||||||
@@ -41,6 +43,10 @@ type ReviewFormData = {
|
|||||||
const ReviewSection: React.FC<ReviewSectionProps> = ({
|
const ReviewSection: React.FC<ReviewSectionProps> = ({
|
||||||
roomId
|
roomId
|
||||||
}) => {
|
}) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
|
||||||
const { isAuthenticated } = useAuthStore();
|
const { isAuthenticated } = useAuthStore();
|
||||||
const { openModal } = useAuthModal();
|
const { openModal } = useAuthModal();
|
||||||
const [reviews, setReviews] = useState<Review[]>([]);
|
const [reviews, setReviews] = useState<Review[]>([]);
|
||||||
@@ -184,8 +190,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{}
|
{}
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20 p-3 sm:p-4 backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5">
|
<div className={`${cardClasses} rounded-lg border border-[var(--luxury-gold)]/20 p-3 sm:p-4 backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5`}>
|
||||||
<h3 className="text-sm sm:text-base font-serif font-semibold text-white mb-3 tracking-wide">
|
<h3 className={`text-sm sm:text-base font-serif font-semibold ${textClasses.primary} mb-3 tracking-wide`}>
|
||||||
Customer Reviews
|
Customer Reviews
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -203,7 +209,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] sm:text-xs text-gray-400 mt-1.5 font-light">
|
<div className={`text-[10px] sm:text-xs ${textClasses.muted} mt-1.5 font-light`}>
|
||||||
{totalReviews > 0
|
{totalReviews > 0
|
||||||
? `${totalReviews} review${totalReviews !== 1 ? 's' : ''}`
|
? `${totalReviews} review${totalReviews !== 1 ? 's' : ''}`
|
||||||
: 'No reviews yet'}
|
: 'No reviews yet'}
|
||||||
@@ -214,8 +220,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20 p-3 sm:p-4 backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5">
|
<div className={`${cardClasses} rounded-lg border border-[var(--luxury-gold)]/20 p-3 sm:p-4 backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5`}>
|
||||||
<h4 className="text-xs sm:text-sm font-serif font-semibold text-white mb-3 tracking-wide">
|
<h4 className={`text-xs sm:text-sm font-serif font-semibold ${textClasses.primary} mb-3 tracking-wide`}>
|
||||||
Write Your Review
|
Write Your Review
|
||||||
</h4>
|
</h4>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}
|
<form onSubmit={handleSubmit(onSubmit)}
|
||||||
@@ -230,8 +236,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] sm:text-xs font-light
|
<label className={`block text-[10px] sm:text-xs font-light
|
||||||
text-gray-300 mb-1.5 tracking-wide"
|
${textClasses.secondary} mb-1.5 tracking-wide`}
|
||||||
>
|
>
|
||||||
Your Rating
|
Your Rating
|
||||||
</label>
|
</label>
|
||||||
@@ -253,8 +259,8 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="comment"
|
htmlFor="comment"
|
||||||
className="block text-[10px] sm:text-xs font-light
|
className={`block text-[10px] sm:text-xs font-light
|
||||||
text-gray-300 mb-1.5 tracking-wide"
|
${textClasses.secondary} mb-1.5 tracking-wide`}
|
||||||
>
|
>
|
||||||
Comment
|
Comment
|
||||||
</label>
|
</label>
|
||||||
@@ -262,11 +268,11 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
{...register('comment')}
|
{...register('comment')}
|
||||||
id="comment"
|
id="comment"
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full px-2.5 py-1.5 bg-[#0a0a0a] border
|
className={`w-full px-2.5 py-1.5 ${inputClasses} border
|
||||||
border-[var(--luxury-gold)]/20 rounded-lg text-white placeholder-gray-500 text-xs sm:text-sm
|
border-[var(--luxury-gold)]/20 rounded-lg placeholder-gray-500 text-xs sm:text-sm
|
||||||
focus:ring-2 focus:ring-[var(--luxury-gold)]/50
|
focus:ring-2 focus:ring-[var(--luxury-gold)]/50
|
||||||
focus:border-[var(--luxury-gold)] transition-all duration-300
|
focus:border-[var(--luxury-gold)] transition-all duration-300
|
||||||
font-light tracking-wide resize-none"
|
font-light tracking-wide resize-none`}
|
||||||
placeholder="Share your experience..."
|
placeholder="Share your experience..."
|
||||||
/>
|
/>
|
||||||
{errors.comment && (
|
{errors.comment && (
|
||||||
@@ -284,7 +290,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
console.error('reCAPTCHA error:', error);
|
console.error('reCAPTCHA error:', error);
|
||||||
setRecaptchaToken(null);
|
setRecaptchaToken(null);
|
||||||
}}
|
}}
|
||||||
theme="dark"
|
theme={theme.theme_layout_mode === 'light' ? 'light' : 'dark'}
|
||||||
size="normal"
|
size="normal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -323,7 +329,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-xs sm:text-sm font-serif font-semibold text-white mb-3 tracking-wide">
|
<h4 className={`text-xs sm:text-sm font-serif font-semibold ${textClasses.primary} mb-3 tracking-wide`}>
|
||||||
All Reviews ({reviews.length > 0 ? reviews.length : totalReviews})
|
All Reviews ({reviews.length > 0 ? reviews.length : totalReviews})
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
@@ -332,25 +338,25 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
{Array.from({ length: 3 }).map((_, index) => (
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20 p-3
|
className={`${cardClasses} rounded-lg border border-[var(--luxury-gold)]/20 p-3
|
||||||
animate-pulse"
|
animate-pulse`}
|
||||||
>
|
>
|
||||||
<div className="h-3 bg-gray-700
|
<div className={`h-3 ${theme.theme_layout_mode === 'light' ? 'bg-gray-300' : 'bg-gray-700'}
|
||||||
rounded w-1/4 mb-2"
|
rounded w-1/4 mb-2`}
|
||||||
/>
|
/>
|
||||||
<div className="h-3 bg-gray-700
|
<div className={`h-3 ${theme.theme_layout_mode === 'light' ? 'bg-gray-300' : 'bg-gray-700'}
|
||||||
rounded w-3/4"
|
rounded w-3/4`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : reviews.length === 0 ? (
|
) : reviews.length === 0 ? (
|
||||||
<div className="text-center py-6 sm:py-8 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20 p-4"
|
<div className={`text-center py-6 sm:py-8 ${cardClasses} rounded-lg border border-[var(--luxury-gold)]/20 p-4`}
|
||||||
>
|
>
|
||||||
<p className="text-gray-300 text-sm sm:text-base font-light">
|
<p className={`${textClasses.secondary} text-sm sm:text-base font-light`}>
|
||||||
No reviews yet
|
No reviews yet
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-400 text-xs sm:text-sm mt-1.5 font-light">
|
<p className={`${textClasses.muted} text-xs sm:text-sm mt-1.5 font-light`}>
|
||||||
Be the first to review this room!
|
Be the first to review this room!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -359,15 +365,15 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
{reviews.map((review) => (
|
{reviews.map((review) => (
|
||||||
<div
|
<div
|
||||||
key={review.id}
|
key={review.id}
|
||||||
className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg border border-[var(--luxury-gold)]/20
|
className={`${cardClasses} rounded-lg border border-[var(--luxury-gold)]/20
|
||||||
p-3 sm:p-4 backdrop-blur-xl shadow-sm shadow-[var(--luxury-gold)]/5"
|
p-3 sm:p-4 backdrop-blur-xl shadow-sm shadow-[var(--luxury-gold)]/5`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start
|
<div className="flex items-start
|
||||||
justify-between mb-2"
|
justify-between mb-2"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h5 className="font-semibold
|
<h5 className={`font-semibold
|
||||||
text-white text-xs sm:text-sm"
|
${textClasses.primary} text-xs sm:text-sm`}
|
||||||
>
|
>
|
||||||
{review.user?.full_name || 'Guest'}
|
{review.user?.full_name || 'Guest'}
|
||||||
</h5>
|
</h5>
|
||||||
@@ -378,15 +384,15 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
rating={review.rating}
|
rating={review.rating}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] sm:text-xs
|
<span className={`text-[10px] sm:text-xs
|
||||||
text-gray-400 font-light"
|
${textClasses.muted} font-light`}
|
||||||
>
|
>
|
||||||
{formatDate(review.created_at)}
|
{formatDate(review.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-300 leading-relaxed text-xs sm:text-sm font-light">
|
<p className={`${textClasses.secondary} leading-relaxed text-xs sm:text-sm font-light`}>
|
||||||
{review.comment}
|
{review.comment}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'react-datepicker/dist/react-datepicker.css';
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
import { useFormatCurrency } from '../../payments/hooks/useFormatCurrency';
|
||||||
import { Calendar, DollarSign, Users, X } from 'lucide-react';
|
import { Calendar, DollarSign, Users, X } from 'lucide-react';
|
||||||
|
import { useTheme } from '../../../shared/contexts/ThemeContext';
|
||||||
|
import { getThemeTextClasses, getThemeCardClasses, getThemeInputClasses } from '../../../shared/utils/themeUtils';
|
||||||
|
|
||||||
interface RoomFilterProps {
|
interface RoomFilterProps {
|
||||||
onFilterChange?: (filters: FilterValues) => void;
|
onFilterChange?: (filters: FilterValues) => void;
|
||||||
@@ -19,6 +21,10 @@ export interface FilterValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
|
const inputClasses = getThemeInputClasses(theme.theme_layout_mode);
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { formatCurrency: formatCurrencyUtil } = useFormatCurrency();
|
const { formatCurrency: formatCurrencyUtil } = useFormatCurrency();
|
||||||
|
|
||||||
@@ -249,10 +255,10 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
<div className={`${cardClasses}
|
||||||
rounded-xl border border-[var(--luxury-gold)]/30
|
rounded-xl border border-[var(--luxury-gold)]/30
|
||||||
backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/10
|
backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/10
|
||||||
p-4 sm:p-5 md:p-6"
|
p-4 sm:p-5 md:p-6`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 sm:gap-3 mb-4 sm:mb-5 md:mb-6">
|
<div className="flex items-center gap-2 sm:gap-3 mb-4 sm:mb-5 md:mb-6">
|
||||||
<div className="p-1.5 sm:p-2 bg-[var(--luxury-gold)]/10 rounded-lg
|
<div className="p-1.5 sm:p-2 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||||
@@ -261,7 +267,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg sm:text-xl font-serif font-semibold mb-0 text-white tracking-tight">
|
<h2 className={`text-lg sm:text-xl font-serif font-semibold mb-0 ${textClasses.primary} tracking-tight`}>
|
||||||
Room Filters
|
Room Filters
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -270,8 +276,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="type"
|
htmlFor="type"
|
||||||
className="block text-sm font-medium
|
className={`block text-sm font-medium
|
||||||
text-gray-200 mb-2 tracking-wide"
|
${textClasses.secondary} mb-2 tracking-wide`}
|
||||||
>
|
>
|
||||||
Room Type
|
Room Type
|
||||||
</label>
|
</label>
|
||||||
@@ -281,20 +287,20 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
name="type"
|
name="type"
|
||||||
value={filters.type || ''}
|
value={filters.type || ''}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="w-full px-4 py-3.5 bg-[#1a1a1a] border-2
|
className={`w-full px-4 py-3.5 ${inputClasses} border-2
|
||||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||||
placeholder-gray-400 focus:ring-2
|
placeholder-gray-400 focus:ring-2
|
||||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||||
transition-all duration-300 font-normal tracking-wide
|
transition-all duration-300 font-normal tracking-wide
|
||||||
appearance-none cursor-pointer
|
appearance-none cursor-pointer
|
||||||
hover:border-[var(--luxury-gold)]/50"
|
hover:border-[var(--luxury-gold)]/50`}
|
||||||
>
|
>
|
||||||
<option value="" className="bg-[#1a1a1a] text-white">All Room Types</option>
|
<option value="" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>All Room Types</option>
|
||||||
<option value="Standard Room" className="bg-[#1a1a1a] text-white">Standard Room</option>
|
<option value="Standard Room" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>Standard Room</option>
|
||||||
<option value="Deluxe Room" className="bg-[#1a1a1a] text-white">Deluxe Room</option>
|
<option value="Deluxe Room" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>Deluxe Room</option>
|
||||||
<option value="Luxury Room" className="bg-[#1a1a1a] text-white">Luxury Room</option>
|
<option value="Luxury Room" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>Luxury Room</option>
|
||||||
<option value="Family Room" className="bg-[#1a1a1a] text-white">Family Room</option>
|
<option value="Family Room" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>Family Room</option>
|
||||||
<option value="Twin Room" className="bg-[#1a1a1a] text-white">Twin Room</option>
|
<option value="Twin Room" className={theme.theme_layout_mode === 'light' ? 'bg-white text-gray-900' : 'bg-[#1a1a1a] text-white'}>Twin Room</option>
|
||||||
</select>
|
</select>
|
||||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||||
<svg className="w-5 h-5 text-[var(--luxury-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5 text-[var(--luxury-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -309,7 +315,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="from"
|
htmlFor="from"
|
||||||
className="block text-sm font-medium text-gray-200 mb-2 tracking-wide flex items-center gap-2"
|
className={`block text-sm font-medium ${textClasses.secondary} mb-2 tracking-wide flex items-center gap-2`}
|
||||||
>
|
>
|
||||||
<Calendar className="w-4 h-4 text-[var(--luxury-gold)]" />
|
<Calendar className="w-4 h-4 text-[var(--luxury-gold)]" />
|
||||||
Check-in Date
|
Check-in Date
|
||||||
@@ -330,12 +336,12 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
minDate={new Date()}
|
minDate={new Date()}
|
||||||
dateFormat="dd/MM/yyyy"
|
dateFormat="dd/MM/yyyy"
|
||||||
placeholderText="Select check-in"
|
placeholderText="Select check-in"
|
||||||
className="w-full px-4 py-3.5 pl-11 bg-[#1a1a1a] border-2
|
className={`w-full px-4 py-3.5 pl-11 ${inputClasses} border-2
|
||||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||||
placeholder-gray-400 focus:ring-2
|
placeholder-gray-400 focus:ring-2
|
||||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||||
transition-all duration-300 font-normal tracking-wide
|
transition-all duration-300 font-normal tracking-wide
|
||||||
hover:border-[var(--luxury-gold)]/50"
|
hover:border-[var(--luxury-gold)]/50`}
|
||||||
wrapperClassName="w-full"
|
wrapperClassName="w-full"
|
||||||
/>
|
/>
|
||||||
{checkInDate && (
|
{checkInDate && (
|
||||||
@@ -348,8 +354,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
setCheckOutDate(null);
|
setCheckOutDate(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2
|
className={`absolute right-3 top-1/2 -translate-y-1/2
|
||||||
text-gray-400 hover:text-[var(--luxury-gold)] transition-colors"
|
${textClasses.muted} hover:text-[var(--luxury-gold)] transition-colors`}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -362,7 +368,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="to"
|
htmlFor="to"
|
||||||
className="block text-sm font-medium text-gray-200 mb-2 tracking-wide flex items-center gap-2"
|
className={`block text-sm font-medium ${textClasses.secondary} mb-2 tracking-wide flex items-center gap-2`}
|
||||||
>
|
>
|
||||||
<Calendar className="w-4 h-4 text-[var(--luxury-gold)]" />
|
<Calendar className="w-4 h-4 text-[var(--luxury-gold)]" />
|
||||||
Check-out Date
|
Check-out Date
|
||||||
@@ -382,13 +388,13 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
disabled={!checkInDate}
|
disabled={!checkInDate}
|
||||||
dateFormat="dd/MM/yyyy"
|
dateFormat="dd/MM/yyyy"
|
||||||
placeholderText={checkInDate ? "Select check-out" : "Select check-in first"}
|
placeholderText={checkInDate ? "Select check-out" : "Select check-in first"}
|
||||||
className="w-full px-4 py-3.5 pl-11 bg-[#1a1a1a] border-2
|
className={`w-full px-4 py-3.5 pl-11 ${inputClasses} border-2
|
||||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||||
placeholder-gray-400 focus:ring-2
|
placeholder-gray-400 focus:ring-2
|
||||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||||
transition-all duration-300 font-normal tracking-wide
|
transition-all duration-300 font-normal tracking-wide
|
||||||
hover:border-[var(--luxury-gold)]/50
|
hover:border-[var(--luxury-gold)]/50
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||||
wrapperClassName="w-full"
|
wrapperClassName="w-full"
|
||||||
/>
|
/>
|
||||||
{checkOutDate && (
|
{checkOutDate && (
|
||||||
@@ -398,8 +404,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setCheckOutDate(null);
|
setCheckOutDate(null);
|
||||||
}}
|
}}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2
|
className={`absolute right-3 top-1/2 -translate-y-1/2
|
||||||
text-gray-400 hover:text-[var(--luxury-gold)] transition-colors"
|
${textClasses.muted} hover:text-[var(--luxury-gold)] transition-colors`}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -408,7 +414,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
w-5 h-5 text-[var(--luxury-gold)] pointer-events-none" />
|
w-5 h-5 text-[var(--luxury-gold)] pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
{checkInDate && !checkOutDate && (
|
{checkInDate && !checkOutDate && (
|
||||||
<p className="mt-1.5 text-xs text-gray-400 font-light">
|
<p className={`mt-1.5 text-xs ${textClasses.muted} font-light`}>
|
||||||
Select check-out date (minimum 1 night stay)
|
Select check-out date (minimum 1 night stay)
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -417,7 +423,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-200 mb-3 tracking-wide flex items-center gap-2">
|
<label className={`block text-sm font-medium ${textClasses.secondary} mb-3 tracking-wide flex items-center gap-2`}>
|
||||||
<DollarSign className="w-4 h-4 text-[var(--luxury-gold)]" />
|
<DollarSign className="w-4 h-4 text-[var(--luxury-gold)]" />
|
||||||
Price Range
|
Price Range
|
||||||
</label>
|
</label>
|
||||||
@@ -425,8 +431,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="minPrice"
|
htmlFor="minPrice"
|
||||||
className="block text-xs font-normal
|
className={`block text-xs font-normal
|
||||||
text-gray-400 mb-1.5 tracking-wide"
|
${textClasses.muted} mb-1.5 tracking-wide`}
|
||||||
>
|
>
|
||||||
Minimum
|
Minimum
|
||||||
</label>
|
</label>
|
||||||
@@ -446,20 +452,20 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
placeholder="0"
|
placeholder="0"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
pattern="[0-9.]*"
|
pattern="[0-9.]*"
|
||||||
className="w-full px-4 py-3.5 pl-10 bg-[#1a1a1a] border-2
|
className={`w-full px-4 py-3.5 pl-10 ${inputClasses} border-2
|
||||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||||
placeholder-gray-400 focus:ring-2
|
placeholder-gray-400 focus:ring-2
|
||||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||||
transition-all duration-300 font-normal tracking-wide
|
transition-all duration-300 font-normal tracking-wide
|
||||||
hover:border-[var(--luxury-gold)]/50"
|
hover:border-[var(--luxury-gold)]/50`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="maxPrice"
|
htmlFor="maxPrice"
|
||||||
className="block text-xs font-normal
|
className={`block text-xs font-normal
|
||||||
text-gray-400 mb-1.5 tracking-wide"
|
${textClasses.muted} mb-1.5 tracking-wide`}
|
||||||
>
|
>
|
||||||
Maximum
|
Maximum
|
||||||
</label>
|
</label>
|
||||||
@@ -479,12 +485,12 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
placeholder="No limit"
|
placeholder="No limit"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
pattern="[0-9.]*"
|
pattern="[0-9.]*"
|
||||||
className="w-full px-4 py-3.5 pl-10 bg-[#1a1a1a] border-2
|
className={`w-full px-4 py-3.5 pl-10 ${inputClasses} border-2
|
||||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||||
placeholder-gray-400 focus:ring-2
|
placeholder-gray-400 focus:ring-2
|
||||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||||
transition-all duration-300 font-normal tracking-wide
|
transition-all duration-300 font-normal tracking-wide
|
||||||
hover:border-[var(--luxury-gold)]/50"
|
hover:border-[var(--luxury-gold)]/50`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -495,8 +501,8 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="capacity"
|
htmlFor="capacity"
|
||||||
className="block text-sm font-medium
|
className={`block text-sm font-medium
|
||||||
text-gray-200 mb-2 tracking-wide flex items-center gap-2"
|
${textClasses.secondary} mb-2 tracking-wide flex items-center gap-2`}
|
||||||
>
|
>
|
||||||
<Users className="w-4 h-4 text-[var(--luxury-gold)]" />
|
<Users className="w-4 h-4 text-[var(--luxury-gold)]" />
|
||||||
Number of Guests
|
Number of Guests
|
||||||
@@ -513,37 +519,37 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
placeholder="1"
|
placeholder="1"
|
||||||
min="1"
|
min="1"
|
||||||
max="10"
|
max="10"
|
||||||
className="w-full px-4 py-3.5 pl-11 bg-[#1a1a1a] border-2
|
className={`w-full px-4 py-3.5 pl-11 ${inputClasses} border-2
|
||||||
border-[var(--luxury-gold)]/30 rounded-lg text-white text-base
|
border-[var(--luxury-gold)]/30 rounded-lg text-base
|
||||||
placeholder-gray-400 focus:ring-2
|
placeholder-gray-400 focus:ring-2
|
||||||
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
focus:ring-[var(--luxury-gold)]/60 focus:border-[var(--luxury-gold)]
|
||||||
transition-all duration-300 font-normal tracking-wide
|
transition-all duration-300 font-normal tracking-wide
|
||||||
hover:border-[var(--luxury-gold)]/50
|
hover:border-[var(--luxury-gold)]/50
|
||||||
[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none
|
[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none
|
||||||
[&::-webkit-inner-spin-button]:appearance-none"
|
[&::-webkit-inner-spin-button]:appearance-none`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
{}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-200 mb-3 tracking-wide">
|
<label className={`block text-sm font-medium ${textClasses.secondary} mb-3 tracking-wide`}>
|
||||||
Amenities
|
Amenities
|
||||||
</label>
|
</label>
|
||||||
{availableAmenities.length === 0 ? (
|
{availableAmenities.length === 0 ? (
|
||||||
<div className="text-sm text-gray-400 font-light bg-[#1a1a1a]/50
|
<div className={`text-sm ${textClasses.muted} font-light ${theme.theme_layout_mode === 'light' ? 'bg-gray-50' : 'bg-[#1a1a1a]/50'}
|
||||||
border border-[var(--luxury-gold)]/20 rounded-lg px-4 py-3">
|
border border-[var(--luxury-gold)]/20 rounded-lg px-4 py-3`}>
|
||||||
Loading amenities...
|
Loading amenities...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-[#1a1a1a]/50 border border-[var(--luxury-gold)]/20 rounded-lg p-3
|
<div className={`${theme.theme_layout_mode === 'light' ? 'bg-gray-50' : 'bg-[#1a1a1a]/50'} border border-[var(--luxury-gold)]/20 rounded-lg p-3
|
||||||
max-h-48 overflow-y-auto custom-scrollbar space-y-2">
|
max-h-48 overflow-y-auto custom-scrollbar space-y-2`}>
|
||||||
{availableAmenities.map((amenity) => (
|
{availableAmenities.map((amenity) => (
|
||||||
<label
|
<label
|
||||||
key={amenity}
|
key={amenity}
|
||||||
className="flex items-center gap-3 text-sm w-full font-normal tracking-wide
|
className={`flex items-center gap-3 text-sm w-full font-normal tracking-wide
|
||||||
hover:text-[var(--luxury-gold)] transition-colors cursor-pointer
|
hover:text-[var(--luxury-gold)] transition-colors cursor-pointer
|
||||||
text-gray-200 group"
|
${textClasses.secondary} group`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -584,11 +590,11 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
className="flex-1 bg-[#0a0a0a] backdrop-blur-sm text-gray-300
|
className={`flex-1 ${theme.theme_layout_mode === 'light' ? 'bg-gray-100' : 'bg-[#0a0a0a]'} backdrop-blur-sm ${textClasses.secondary}
|
||||||
py-2.5 sm:py-3 px-4 rounded-sm border border-[var(--luxury-gold)]/30
|
py-2.5 sm:py-3 px-4 rounded-sm border border-[var(--luxury-gold)]/30
|
||||||
hover:bg-[#1a1a1a] hover:border-[var(--luxury-gold)] hover:text-[var(--luxury-gold)] active:scale-95
|
${theme.theme_layout_mode === 'light' ? 'hover:bg-gray-200' : 'hover:bg-[#1a1a1a]'} hover:border-[var(--luxury-gold)] hover:text-[var(--luxury-gold)] active:scale-95
|
||||||
transition-all font-medium tracking-wide text-sm sm:text-base
|
transition-all font-medium tracking-wide text-sm sm:text-base
|
||||||
touch-manipulation min-h-[44px]"
|
touch-manipulation min-h-[44px]`}
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// Normalize value - ensure it's never an empty string, use a default if needed
|
||||||
|
const normalizedValue = value && value.trim() ? value : undefined;
|
||||||
|
|
||||||
const allIcons = useMemo(() => {
|
const allIcons = useMemo(() => {
|
||||||
const icons: string[] = [];
|
const icons: string[] = [];
|
||||||
@@ -59,25 +61,57 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
|||||||
'memo'
|
'memo'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for (const iconName in LucideIcons) {
|
try {
|
||||||
|
// Check if LucideIcons is available
|
||||||
if (
|
if (!LucideIcons || typeof LucideIcons !== 'object') {
|
||||||
excludedNames.has(iconName) ||
|
console.error('IconPicker: LucideIcons is not available');
|
||||||
iconName.startsWith('_') ||
|
// Return popular icons as fallback
|
||||||
iconName[0] !== iconName[0].toUpperCase()
|
return popularIcons.filter(icon => icon);
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconComponent = (LucideIcons as any)[iconName];
|
for (const iconName in LucideIcons) {
|
||||||
|
// Skip if excluded
|
||||||
|
if (excludedNames.has(iconName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof iconComponent === 'function') {
|
// Skip if starts with underscore
|
||||||
icons.push(iconName);
|
if (iconName.startsWith('_')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if empty or doesn't start with uppercase letter
|
||||||
|
if (!iconName || iconName.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if first character is uppercase letter
|
||||||
|
const firstChar = iconName.charAt(0);
|
||||||
|
if (firstChar < 'A' || firstChar > 'Z') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconComponent = (LucideIcons as any)[iconName];
|
||||||
|
|
||||||
|
// Only include if it's a function (React component)
|
||||||
|
if (typeof iconComponent === 'function') {
|
||||||
|
icons.push(iconName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no icons found, use popular icons as fallback
|
||||||
|
if (icons.length === 0) {
|
||||||
|
console.warn('IconPicker: No icons found, using popular icons as fallback');
|
||||||
|
return popularIcons.filter(icon => icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = icons.sort();
|
||||||
|
return sorted;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('IconPicker: Error loading icons:', error);
|
||||||
|
// Return popular icons as fallback
|
||||||
|
return popularIcons.filter(icon => icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sorted = icons.sort();
|
|
||||||
return sorted;
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
@@ -93,7 +127,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
|||||||
);
|
);
|
||||||
}, [searchQuery, allIcons]);
|
}, [searchQuery, allIcons]);
|
||||||
|
|
||||||
const selectedIcon = value && (LucideIcons as any)[value] ? (LucideIcons as any)[value] : null;
|
const selectedIcon = normalizedValue && (LucideIcons as any)[normalizedValue] ? (LucideIcons as any)[normalizedValue] : null;
|
||||||
|
|
||||||
const handleIconSelect = (iconName: string) => {
|
const handleIconSelect = (iconName: string) => {
|
||||||
onChange(iconName);
|
onChange(iconName);
|
||||||
@@ -113,7 +147,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
|||||||
{selectedIcon ? (
|
{selectedIcon ? (
|
||||||
<>
|
<>
|
||||||
{React.createElement(selectedIcon, { className: 'w-5 h-5 text-gray-700' })}
|
{React.createElement(selectedIcon, { className: 'w-5 h-5 text-gray-700' })}
|
||||||
<span className="text-gray-700 font-medium">{value}</span>
|
<span className="text-gray-700 font-medium">{normalizedValue}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400">Select an icon</span>
|
<span className="text-gray-400">Select an icon</span>
|
||||||
@@ -152,7 +186,12 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto p-4">
|
<div className="overflow-y-auto p-4">
|
||||||
{filteredIcons.length > 0 ? (
|
{allIcons.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<p className="text-sm">Unable to load icons. Please refresh the page.</p>
|
||||||
|
<p className="text-xs mt-2">If the problem persists, check the browser console for errors.</p>
|
||||||
|
</div>
|
||||||
|
) : filteredIcons.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{!searchQuery.trim() && (
|
{!searchQuery.trim() && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
@@ -166,7 +205,7 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
|||||||
const IconComponent = (LucideIcons as any)[iconName];
|
const IconComponent = (LucideIcons as any)[iconName];
|
||||||
if (!IconComponent) return null;
|
if (!IconComponent) return null;
|
||||||
|
|
||||||
const isSelected = value === iconName;
|
const isSelected = normalizedValue === iconName;
|
||||||
const isPopular = !searchQuery.trim() && popularIcons.includes(iconName);
|
const isPopular = !searchQuery.trim() && popularIcons.includes(iconName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -208,8 +247,14 @@ const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, label = 'Icon'
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500">
|
||||||
<p>No icons found matching "{searchQuery}"</p>
|
{searchQuery.trim() ? (
|
||||||
<p className="text-xs mt-2">Try a different search term</p>
|
<>
|
||||||
|
<p>No icons found matching "{searchQuery}"</p>
|
||||||
|
<p className="text-xs mt-2">Try a different search term</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm">No icons available</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ export interface CompanySettingsResponse {
|
|||||||
tax_rate: number;
|
tax_rate: number;
|
||||||
chat_working_hours_start: number;
|
chat_working_hours_start: number;
|
||||||
chat_working_hours_end: number;
|
chat_working_hours_end: number;
|
||||||
|
bank_name?: string;
|
||||||
|
bank_account_number?: string;
|
||||||
|
bank_account_holder?: string;
|
||||||
|
bank_code?: string;
|
||||||
updated_at?: string | null;
|
updated_at?: string | null;
|
||||||
updated_by?: string | null;
|
updated_by?: string | null;
|
||||||
};
|
};
|
||||||
@@ -154,6 +158,10 @@ export interface UpdateCompanySettingsRequest {
|
|||||||
tax_rate?: number;
|
tax_rate?: number;
|
||||||
chat_working_hours_start?: number;
|
chat_working_hours_start?: number;
|
||||||
chat_working_hours_end?: number;
|
chat_working_hours_end?: number;
|
||||||
|
bank_name?: string;
|
||||||
|
bank_account_number?: string;
|
||||||
|
bank_account_holder?: string;
|
||||||
|
bank_code?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadLogoResponse {
|
export interface UploadLogoResponse {
|
||||||
@@ -219,6 +227,7 @@ export interface ThemeSettingsResponse {
|
|||||||
theme_primary_light: string;
|
theme_primary_light: string;
|
||||||
theme_primary_dark: string;
|
theme_primary_dark: string;
|
||||||
theme_primary_accent: string;
|
theme_primary_accent: string;
|
||||||
|
theme_layout_mode: string; // 'dark' or 'light'
|
||||||
updated_at?: string | null;
|
updated_at?: string | null;
|
||||||
updated_by?: string | null;
|
updated_by?: string | null;
|
||||||
};
|
};
|
||||||
@@ -230,6 +239,7 @@ export interface UpdateThemeSettingsRequest {
|
|||||||
theme_primary_light?: string;
|
theme_primary_light?: string;
|
||||||
theme_primary_dark?: string;
|
theme_primary_dark?: string;
|
||||||
theme_primary_accent?: string;
|
theme_primary_accent?: string;
|
||||||
|
theme_layout_mode?: string; // 'dark' or 'light'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VerifyRecaptchaRequest {
|
export interface VerifyRecaptchaRequest {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const CurrencySettingsPage: React.FC = () => {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const currencyNames: Record<string, string> = {
|
const currencyNames: Record<string, string> = {
|
||||||
VND: 'Vietnamese Dong',
|
|
||||||
USD: 'US Dollar',
|
USD: 'US Dollar',
|
||||||
EUR: 'Euro',
|
EUR: 'Euro',
|
||||||
GBP: 'British Pound',
|
GBP: 'British Pound',
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
services_section_button_text: contents.home.services_section_button_text || '',
|
services_section_button_text: contents.home.services_section_button_text || '',
|
||||||
services_section_button_link: contents.home.services_section_button_link || '',
|
services_section_button_link: contents.home.services_section_button_link || '',
|
||||||
services_section_limit: contents.home.services_section_limit || 6,
|
services_section_limit: contents.home.services_section_limit || 6,
|
||||||
|
services_fallback_icon: contents.home.services_fallback_icon || 'Award',
|
||||||
luxury_experiences_section_title: contents.home.luxury_experiences_section_title || '',
|
luxury_experiences_section_title: contents.home.luxury_experiences_section_title || '',
|
||||||
luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '',
|
luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '',
|
||||||
luxury_experiences: normalizeArray(contents.home.luxury_experiences),
|
luxury_experiences: normalizeArray(contents.home.luxury_experiences),
|
||||||
@@ -284,19 +285,56 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
|
|
||||||
// Contact
|
// Contact
|
||||||
if (contents.contact) {
|
if (contents.contact) {
|
||||||
|
const defaultContactIcons = {
|
||||||
|
hero: 'Mail',
|
||||||
|
email: 'Mail',
|
||||||
|
phone: 'Phone',
|
||||||
|
location: 'MapPin',
|
||||||
|
name_field: 'User',
|
||||||
|
email_field: 'Mail',
|
||||||
|
phone_field: 'Phone',
|
||||||
|
subject_field: 'MessageSquare',
|
||||||
|
message_field: 'MessageSquare',
|
||||||
|
submit_button: 'Send',
|
||||||
|
};
|
||||||
setContactData({
|
setContactData({
|
||||||
title: contents.contact.title || '',
|
title: contents.contact.title || '',
|
||||||
subtitle: contents.contact.subtitle || '',
|
subtitle: contents.contact.subtitle || '',
|
||||||
description: contents.contact.description || '',
|
description: contents.contact.description || '',
|
||||||
content: contents.contact.content || '',
|
content: contents.contact.content || '',
|
||||||
map_url: contents.contact.map_url || '',
|
map_url: contents.contact.map_url || '',
|
||||||
|
contact_icons: contents.contact.contact_icons ? {
|
||||||
|
...defaultContactIcons,
|
||||||
|
...contents.contact.contact_icons,
|
||||||
|
} : defaultContactIcons,
|
||||||
meta_title: contents.contact.meta_title || '',
|
meta_title: contents.contact.meta_title || '',
|
||||||
meta_description: contents.contact.meta_description || '',
|
meta_description: contents.contact.meta_description || '',
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Initialize with defaults even if no contact data exists
|
||||||
|
setContactData({
|
||||||
|
contact_icons: {
|
||||||
|
hero: 'Mail',
|
||||||
|
email: 'Mail',
|
||||||
|
phone: 'Phone',
|
||||||
|
location: 'MapPin',
|
||||||
|
name_field: 'User',
|
||||||
|
email_field: 'Mail',
|
||||||
|
phone_field: 'Phone',
|
||||||
|
subject_field: 'MessageSquare',
|
||||||
|
message_field: 'MessageSquare',
|
||||||
|
submit_button: 'Send',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// About
|
// About
|
||||||
if (contents.about) {
|
if (contents.about) {
|
||||||
|
const defaultAboutContactIcons = {
|
||||||
|
location: 'MapPin',
|
||||||
|
phone: 'Phone',
|
||||||
|
email: 'Mail',
|
||||||
|
};
|
||||||
setAboutData({
|
setAboutData({
|
||||||
title: contents.about.title || '',
|
title: contents.about.title || '',
|
||||||
subtitle: contents.about.subtitle || '',
|
subtitle: contents.about.subtitle || '',
|
||||||
@@ -306,6 +344,12 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
values: normalizeArray(contents.about.values),
|
values: normalizeArray(contents.about.values),
|
||||||
features: normalizeArray(contents.about.features),
|
features: normalizeArray(contents.about.features),
|
||||||
about_hero_image: contents.about.about_hero_image || '',
|
about_hero_image: contents.about.about_hero_image || '',
|
||||||
|
about_hero_icon: contents.about.about_hero_icon || 'Hotel',
|
||||||
|
about_contact_icons: contents.about.about_contact_icons ? {
|
||||||
|
...defaultAboutContactIcons,
|
||||||
|
...contents.about.about_contact_icons,
|
||||||
|
} : defaultAboutContactIcons,
|
||||||
|
about_learn_more_icon: contents.about.about_learn_more_icon || 'Hotel',
|
||||||
mission: contents.about.mission || '',
|
mission: contents.about.mission || '',
|
||||||
vision: contents.about.vision || '',
|
vision: contents.about.vision || '',
|
||||||
team: normalizeArray(contents.about.team),
|
team: normalizeArray(contents.about.team),
|
||||||
@@ -314,6 +358,17 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
meta_title: contents.about.meta_title || '',
|
meta_title: contents.about.meta_title || '',
|
||||||
meta_description: contents.about.meta_description || '',
|
meta_description: contents.about.meta_description || '',
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Initialize with defaults even if no about data exists
|
||||||
|
setAboutData({
|
||||||
|
about_hero_icon: 'Hotel',
|
||||||
|
about_contact_icons: {
|
||||||
|
location: 'MapPin',
|
||||||
|
phone: 'Phone',
|
||||||
|
email: 'Mail',
|
||||||
|
},
|
||||||
|
about_learn_more_icon: 'Hotel',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
@@ -1354,7 +1409,7 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
value={amenity?.icon || ''}
|
value={amenity?.icon && amenity.icon.trim() ? amenity.icon : 'Sparkles'}
|
||||||
onChange={(iconName) => {
|
onChange={(iconName) => {
|
||||||
setHomeData((prevData) => {
|
setHomeData((prevData) => {
|
||||||
const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
|
const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
|
||||||
@@ -1550,7 +1605,7 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
value={feature?.icon || ''}
|
value={feature?.icon && feature.icon.trim() ? feature.icon : 'Star'}
|
||||||
onChange={(iconName) => {
|
onChange={(iconName) => {
|
||||||
setHomeData((prevData) => {
|
setHomeData((prevData) => {
|
||||||
const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
|
const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
|
||||||
@@ -2031,7 +2086,7 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
value={stat?.icon || ''}
|
value={stat?.icon && stat.icon.trim() ? stat.icon : 'TrendingUp'}
|
||||||
onChange={(iconName) => {
|
onChange={(iconName) => {
|
||||||
const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
|
const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
|
||||||
currentStats[index] = { ...currentStats[index], icon: iconName };
|
currentStats[index] = { ...currentStats[index], icon: iconName };
|
||||||
@@ -2177,7 +2232,7 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
value={feature?.icon || ''}
|
value={feature?.icon && feature.icon.trim() ? feature.icon : 'Star'}
|
||||||
onChange={(iconName) => {
|
onChange={(iconName) => {
|
||||||
setHomeData((prevData) => {
|
setHomeData((prevData) => {
|
||||||
const currentFeatures = Array.isArray(prevData.features) ? [...prevData.features] : [];
|
const currentFeatures = Array.isArray(prevData.features) ? [...prevData.features] : [];
|
||||||
@@ -2303,6 +2358,14 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
placeholder="6"
|
placeholder="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Services Fallback Icon</label>
|
||||||
|
<p className="text-xs text-gray-500 mb-2">Icon shown when a service has no icon set</p>
|
||||||
|
<IconPicker
|
||||||
|
value={homeData.services_fallback_icon && homeData.services_fallback_icon.trim() ? homeData.services_fallback_icon : 'Award'}
|
||||||
|
onChange={(icon) => setHomeData({ ...homeData, services_fallback_icon: icon })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2371,7 +2434,7 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
value={experience?.icon || ''}
|
value={experience?.icon && experience.icon.trim() ? experience.icon : 'Sparkles'}
|
||||||
onChange={(iconName) => {
|
onChange={(iconName) => {
|
||||||
const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
|
const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
|
||||||
current[index] = { ...current[index], icon: iconName };
|
current[index] = { ...current[index], icon: iconName };
|
||||||
@@ -2522,7 +2585,7 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
value={award?.icon || ''}
|
value={award?.icon && award.icon.trim() ? award.icon : 'Award'}
|
||||||
onChange={(iconName) => {
|
onChange={(iconName) => {
|
||||||
const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
|
const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
|
||||||
current[index] = { ...current[index], icon: iconName };
|
current[index] = { ...current[index], icon: iconName };
|
||||||
@@ -3607,6 +3670,112 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-200 pt-6">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-4">Icons</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Hero Section Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={contactData.contact_icons?.hero && contactData.contact_icons.hero.trim() ? contactData.contact_icons.hero : 'Mail'}
|
||||||
|
onChange={(icon) => setContactData({
|
||||||
|
...contactData,
|
||||||
|
contact_icons: { ...contactData.contact_icons, hero: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Email Contact Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={contactData.contact_icons?.email && contactData.contact_icons.email.trim() ? contactData.contact_icons.email : 'Mail'}
|
||||||
|
onChange={(icon) => setContactData({
|
||||||
|
...contactData,
|
||||||
|
contact_icons: { ...contactData.contact_icons, email: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Phone Contact Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={contactData.contact_icons?.phone && contactData.contact_icons.phone.trim() ? contactData.contact_icons.phone : 'Phone'}
|
||||||
|
onChange={(icon) => setContactData({
|
||||||
|
...contactData,
|
||||||
|
contact_icons: { ...contactData.contact_icons, phone: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Location Contact Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={contactData.contact_icons?.location && contactData.contact_icons.location.trim() ? contactData.contact_icons.location : 'MapPin'}
|
||||||
|
onChange={(icon) => setContactData({
|
||||||
|
...contactData,
|
||||||
|
contact_icons: { ...contactData.contact_icons, location: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Name Field Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={contactData.contact_icons?.name_field && contactData.contact_icons.name_field.trim() ? contactData.contact_icons.name_field : 'User'}
|
||||||
|
onChange={(icon) => setContactData({
|
||||||
|
...contactData,
|
||||||
|
contact_icons: { ...contactData.contact_icons, name_field: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Email Field Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={contactData.contact_icons?.email_field && contactData.contact_icons.email_field.trim() ? contactData.contact_icons.email_field : 'Mail'}
|
||||||
|
onChange={(icon) => setContactData({
|
||||||
|
...contactData,
|
||||||
|
contact_icons: { ...contactData.contact_icons, email_field: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Phone Field Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={contactData.contact_icons?.phone_field && contactData.contact_icons.phone_field.trim() ? contactData.contact_icons.phone_field : 'Phone'}
|
||||||
|
onChange={(icon) => setContactData({
|
||||||
|
...contactData,
|
||||||
|
contact_icons: { ...contactData.contact_icons, phone_field: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Subject Field Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={contactData.contact_icons?.subject_field && contactData.contact_icons.subject_field.trim() ? contactData.contact_icons.subject_field : 'MessageSquare'}
|
||||||
|
onChange={(icon) => setContactData({
|
||||||
|
...contactData,
|
||||||
|
contact_icons: { ...contactData.contact_icons, subject_field: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Message Field Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={contactData.contact_icons?.message_field && contactData.contact_icons.message_field.trim() ? contactData.contact_icons.message_field : 'MessageSquare'}
|
||||||
|
onChange={(icon) => setContactData({
|
||||||
|
...contactData,
|
||||||
|
contact_icons: { ...contactData.contact_icons, message_field: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Submit Button Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={contactData.contact_icons?.submit_button && contactData.contact_icons.submit_button.trim() ? contactData.contact_icons.submit_button : 'Send'}
|
||||||
|
onChange={(icon) => setContactData({
|
||||||
|
...contactData,
|
||||||
|
contact_icons: { ...contactData.contact_icons, submit_button: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
||||||
@@ -3727,6 +3896,15 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hero Icon (when no hero image) */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Hero Icon (shown when no hero image)</label>
|
||||||
|
<IconPicker
|
||||||
|
value={aboutData.about_hero_icon && aboutData.about_hero_icon.trim() ? aboutData.about_hero_icon : 'Hotel'}
|
||||||
|
onChange={(icon) => setAboutData({ ...aboutData, about_hero_icon: icon })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mission */}
|
{/* Mission */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Mission Statement</label>
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Mission Statement</label>
|
||||||
@@ -3792,7 +3970,7 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
value={value.icon || 'Heart'}
|
value={value.icon && value.icon.trim() ? value.icon : 'Heart'}
|
||||||
onChange={(icon: string) => {
|
onChange={(icon: string) => {
|
||||||
setAboutData((prevData) => {
|
setAboutData((prevData) => {
|
||||||
const newValues = [...(prevData.values || [])];
|
const newValues = [...(prevData.values || [])];
|
||||||
@@ -3883,7 +4061,7 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
value={feature.icon || 'Star'}
|
value={feature.icon && feature.icon.trim() ? feature.icon : 'Star'}
|
||||||
onChange={(icon: string) => {
|
onChange={(icon: string) => {
|
||||||
setAboutData((prevData) => {
|
setAboutData((prevData) => {
|
||||||
const newFeatures = [...(prevData.features || [])];
|
const newFeatures = [...(prevData.features || [])];
|
||||||
@@ -4262,7 +4440,7 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
value={achievement.icon || 'Award'}
|
value={achievement.icon && achievement.icon.trim() ? achievement.icon : 'Award'}
|
||||||
onChange={(icon: string) => {
|
onChange={(icon: string) => {
|
||||||
setAboutData((prevData) => {
|
setAboutData((prevData) => {
|
||||||
const newAchievements = [...(prevData.achievements || [])];
|
const newAchievements = [...(prevData.achievements || [])];
|
||||||
@@ -4378,6 +4556,50 @@ const PageContentDashboard: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Additional Icons */}
|
||||||
|
<div className="border-t border-gray-200 pt-6">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 mb-4">Additional Icons</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Contact Info - Location Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={aboutData.about_contact_icons?.location && aboutData.about_contact_icons.location.trim() ? aboutData.about_contact_icons.location : 'MapPin'}
|
||||||
|
onChange={(icon) => setAboutData({
|
||||||
|
...aboutData,
|
||||||
|
about_contact_icons: { ...aboutData.about_contact_icons, location: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Contact Info - Phone Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={aboutData.about_contact_icons?.phone && aboutData.about_contact_icons.phone.trim() ? aboutData.about_contact_icons.phone : 'Phone'}
|
||||||
|
onChange={(icon) => setAboutData({
|
||||||
|
...aboutData,
|
||||||
|
about_contact_icons: { ...aboutData.about_contact_icons, phone: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Contact Info - Email Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={aboutData.about_contact_icons?.email && aboutData.about_contact_icons.email.trim() ? aboutData.about_contact_icons.email : 'Mail'}
|
||||||
|
onChange={(icon) => setAboutData({
|
||||||
|
...aboutData,
|
||||||
|
about_contact_icons: { ...aboutData.about_contact_icons, email: icon }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Learn More Button Icon</label>
|
||||||
|
<IconPicker
|
||||||
|
value={aboutData.about_learn_more_icon && aboutData.about_learn_more_icon.trim() ? aboutData.about_learn_more_icon : 'Hotel'}
|
||||||
|
onChange={(icon) => setAboutData({ ...aboutData, about_learn_more_icon: icon })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
||||||
|
|||||||
@@ -134,6 +134,10 @@ const SettingsPage: React.FC = () => {
|
|||||||
tax_rate: 0,
|
tax_rate: 0,
|
||||||
chat_working_hours_start: 9,
|
chat_working_hours_start: 9,
|
||||||
chat_working_hours_end: 17,
|
chat_working_hours_end: 17,
|
||||||
|
bank_name: '',
|
||||||
|
bank_account_number: '',
|
||||||
|
bank_account_holder: '',
|
||||||
|
bank_code: '',
|
||||||
});
|
});
|
||||||
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||||
const [faviconPreview, setFaviconPreview] = useState<string | null>(null);
|
const [faviconPreview, setFaviconPreview] = useState<string | null>(null);
|
||||||
@@ -156,6 +160,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
theme_primary_light: '#f5d76e',
|
theme_primary_light: '#f5d76e',
|
||||||
theme_primary_dark: '#c9a227',
|
theme_primary_dark: '#c9a227',
|
||||||
theme_primary_accent: '#e8c547',
|
theme_primary_accent: '#e8c547',
|
||||||
|
theme_layout_mode: 'dark',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -165,7 +170,6 @@ const SettingsPage: React.FC = () => {
|
|||||||
const [openPaymentModal, setOpenPaymentModal] = useState<'stripe' | 'paypal' | 'borica' | null>(null);
|
const [openPaymentModal, setOpenPaymentModal] = useState<'stripe' | 'paypal' | 'borica' | null>(null);
|
||||||
|
|
||||||
const currencyNames: Record<string, string> = {
|
const currencyNames: Record<string, string> = {
|
||||||
VND: 'Vietnamese Dong',
|
|
||||||
USD: 'US Dollar',
|
USD: 'US Dollar',
|
||||||
EUR: 'Euro',
|
EUR: 'Euro',
|
||||||
GBP: 'British Pound',
|
GBP: 'British Pound',
|
||||||
@@ -300,6 +304,10 @@ const SettingsPage: React.FC = () => {
|
|||||||
tax_rate: companyRes.data.tax_rate || 0,
|
tax_rate: companyRes.data.tax_rate || 0,
|
||||||
chat_working_hours_start: companyRes.data.chat_working_hours_start || 9,
|
chat_working_hours_start: companyRes.data.chat_working_hours_start || 9,
|
||||||
chat_working_hours_end: companyRes.data.chat_working_hours_end || 17,
|
chat_working_hours_end: companyRes.data.chat_working_hours_end || 17,
|
||||||
|
bank_name: companyRes.data.bank_name || '',
|
||||||
|
bank_account_number: companyRes.data.bank_account_number || '',
|
||||||
|
bank_account_holder: companyRes.data.bank_account_holder || '',
|
||||||
|
bank_code: companyRes.data.bank_code || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -804,6 +812,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
theme_primary_light: themeRes.data.theme_primary_light || '#f5d76e',
|
theme_primary_light: themeRes.data.theme_primary_light || '#f5d76e',
|
||||||
theme_primary_dark: themeRes.data.theme_primary_dark || '#c9a227',
|
theme_primary_dark: themeRes.data.theme_primary_dark || '#c9a227',
|
||||||
theme_primary_accent: themeRes.data.theme_primary_accent || '#e8c547',
|
theme_primary_accent: themeRes.data.theme_primary_accent || '#e8c547',
|
||||||
|
theme_layout_mode: themeRes.data.theme_layout_mode || 'dark',
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(
|
toast.error(
|
||||||
@@ -2722,6 +2731,82 @@ const SettingsPage: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Bank Details Section */}
|
||||||
|
<div className="space-y-4 pt-6 border-t border-gray-200">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Building2 className="w-5 h-5 text-[var(--luxury-gold)]" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Bank Transfer Details</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Configure bank details for bank transfer payments. These will be displayed on payment confirmation pages.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
||||||
|
Bank Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={companyFormData.bank_name || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCompanyFormData({ ...companyFormData, bank_name: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="e.g., Bank of America, Chase Bank"
|
||||||
|
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
||||||
|
Account Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={companyFormData.bank_account_number || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCompanyFormData({ ...companyFormData, bank_account_number: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Bank account number"
|
||||||
|
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
||||||
|
Account Holder Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={companyFormData.bank_account_holder || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCompanyFormData({ ...companyFormData, bank_account_holder: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Name on the bank account"
|
||||||
|
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-900 mb-2">
|
||||||
|
Bank Code (Optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={companyFormData.bank_code || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCompanyFormData({ ...companyFormData, bank_code: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Bank routing/swift code (if needed for QR codes)"
|
||||||
|
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Optional: Bank code used for QR code generation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{}
|
{}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
||||||
@@ -3124,14 +3209,79 @@ const SettingsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Layout Mode Section */}
|
||||||
|
<div className="pt-6 border-t border-gray-200">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide mb-3">
|
||||||
|
<Palette className="w-4 h-4 text-gray-600" />
|
||||||
|
Page Layout Theme
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-gray-600 mb-4">
|
||||||
|
Choose between dark (black) or light (white) background theme for all frontend pages
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setThemeFormData({ ...themeFormData, theme_layout_mode: 'dark' })}
|
||||||
|
className={`p-6 rounded-xl border-2 transition-all duration-200 ${
|
||||||
|
themeFormData.theme_layout_mode === 'dark'
|
||||||
|
? 'border-amber-500 bg-amber-50 shadow-lg'
|
||||||
|
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<div className="w-16 h-16 rounded-lg bg-gradient-to-br from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] border-2 border-gray-300 shadow-inner"></div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className={`font-semibold text-sm ${themeFormData.theme_layout_mode === 'dark' ? 'text-amber-700' : 'text-gray-700'}`}>
|
||||||
|
Dark Theme
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Black backgrounds</p>
|
||||||
|
</div>
|
||||||
|
{themeFormData.theme_layout_mode === 'dark' && (
|
||||||
|
<div className="w-5 h-5 rounded-full bg-amber-500 flex items-center justify-center">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setThemeFormData({ ...themeFormData, theme_layout_mode: 'light' })}
|
||||||
|
className={`p-6 rounded-xl border-2 transition-all duration-200 ${
|
||||||
|
themeFormData.theme_layout_mode === 'light'
|
||||||
|
? 'border-amber-500 bg-amber-50 shadow-lg'
|
||||||
|
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<div className="w-16 h-16 rounded-lg bg-gradient-to-br from-gray-50 via-white to-gray-50 border-2 border-gray-300 shadow-inner"></div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className={`font-semibold text-sm ${themeFormData.theme_layout_mode === 'light' ? 'text-amber-700' : 'text-gray-700'}`}>
|
||||||
|
Light Theme
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">White backgrounds</p>
|
||||||
|
</div>
|
||||||
|
{themeFormData.theme_layout_mode === 'light' && (
|
||||||
|
<div className="w-5 h-5 rounded-full bg-amber-500 flex items-center justify-center">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||||
<div className="text-sm text-blue-800">
|
<div className="text-sm text-blue-800">
|
||||||
<p className="font-semibold mb-1">About Theme Colors</p>
|
<p className="font-semibold mb-1">About Theme Settings</p>
|
||||||
<p className="text-xs">
|
<p className="text-xs">
|
||||||
Changes to theme colors will be applied immediately across all frontend pages.
|
Changes to theme colors and layout mode will be applied immediately across all frontend pages.
|
||||||
Use hex color codes (e.g., #d4af37) for best results. The colors will replace the default gold/yellow theme throughout the application.
|
Use hex color codes (e.g., #d4af37) for best results. The layout mode controls whether pages use dark (black) or light (white) backgrounds.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,12 +39,14 @@ import { getBookingStatusConfig } from '../../shared/utils/bookingUtils';
|
|||||||
import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils';
|
import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils';
|
||||||
import { BOOKING_STATUS, PAYMENT_METHOD, PAYMENT_STATUS, PAYMENT_TYPE } from '../../shared/constants/bookingConstants';
|
import { BOOKING_STATUS, PAYMENT_METHOD, PAYMENT_STATUS, PAYMENT_TYPE } from '../../shared/constants/bookingConstants';
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { useCompanySettings } from '../../shared/contexts/CompanySettingsContext';
|
||||||
|
|
||||||
const BookingSuccessPage: React.FC = () => {
|
const BookingSuccessPage: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { formatCurrency } = useFormatCurrency();
|
const { formatCurrency } = useFormatCurrency();
|
||||||
const { userInfo } = useAuthStore();
|
const { userInfo } = useAuthStore();
|
||||||
|
const { settings } = useCompanySettings();
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
const [booking, setBooking] = useState<Booking | null>(
|
const [booking, setBooking] = useState<Booking | null>(
|
||||||
@@ -260,7 +262,10 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
const qrCodeUrl = booking
|
const qrCodeUrl = booking
|
||||||
? generateQRCode(
|
? generateQRCode(
|
||||||
booking.booking_number,
|
booking.booking_number,
|
||||||
booking.total_price
|
booking.total_price,
|
||||||
|
settings.bank_code || undefined,
|
||||||
|
settings.bank_account_number || undefined,
|
||||||
|
settings.bank_account_holder || undefined
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -687,15 +692,15 @@ const BookingSuccessPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<strong>Bank:</strong>
|
<strong>Bank:</strong>
|
||||||
Vietcombank (VCB)
|
{settings.bank_name || 'Not configured'}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Account Number:</strong>
|
<strong>Account Number:</strong>
|
||||||
0123456789
|
{settings.bank_account_number || 'Not configured'}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Account Holder:</strong>
|
<strong>Account Holder:</strong>
|
||||||
KHACH SAN ABC
|
{settings.bank_account_holder || 'Not configured'}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Amount:</strong>{' '}
|
<strong>Amount:</strong>{' '}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { validateBookingId } from '../../shared/utils/routeValidation';
|
|||||||
import { validateAndHandleBookingOwnership } from '../../shared/utils/ownershipValidation';
|
import { validateAndHandleBookingOwnership } from '../../shared/utils/ownershipValidation';
|
||||||
import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils';
|
import { getPaymentMethodLabel } from '../../shared/utils/paymentUtils';
|
||||||
import { PAYMENT_METHOD, PAYMENT_STATUS } from '../../shared/constants/bookingConstants';
|
import { PAYMENT_METHOD, PAYMENT_STATUS } from '../../shared/constants/bookingConstants';
|
||||||
|
import { useCompanySettings } from '../../shared/contexts/CompanySettingsContext';
|
||||||
|
|
||||||
const PaymentConfirmationPage: React.FC = () => {
|
const PaymentConfirmationPage: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@@ -37,6 +38,7 @@ const PaymentConfirmationPage: React.FC = () => {
|
|||||||
const { isAuthenticated, userInfo } = useAuthStore();
|
const { isAuthenticated, userInfo } = useAuthStore();
|
||||||
const { openModal } = useAuthModal();
|
const { openModal } = useAuthModal();
|
||||||
const { formatCurrency } = useFormatCurrency();
|
const { formatCurrency } = useFormatCurrency();
|
||||||
|
const { settings } = useCompanySettings();
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
const [booking, setBooking] = useState<Booking | null>(
|
const [booking, setBooking] = useState<Booking | null>(
|
||||||
@@ -256,7 +258,10 @@ const PaymentConfirmationPage: React.FC = () => {
|
|||||||
|
|
||||||
const qrCodeUrl = generateQRCode(
|
const qrCodeUrl = generateQRCode(
|
||||||
booking.booking_number,
|
booking.booking_number,
|
||||||
booking.total_price
|
booking.total_price,
|
||||||
|
settings.bank_code || undefined,
|
||||||
|
settings.bank_account_number || undefined,
|
||||||
|
settings.bank_account_holder || undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -371,15 +376,15 @@ const PaymentConfirmationPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<strong>Bank:</strong>
|
<strong>Bank:</strong>
|
||||||
Vietcombank (VCB)
|
{settings.bank_name || 'Not configured'}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Account Number:</strong>
|
<strong>Account Number:</strong>
|
||||||
0123456789
|
{settings.bank_account_number || 'Not configured'}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Account Holder:</strong>
|
<strong>Account Holder:</strong>
|
||||||
KHACH SAN ABC
|
{settings.bank_account_holder || 'Not configured'}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Amount:</strong>{' '}
|
<strong>Amount:</strong>{' '}
|
||||||
@@ -408,12 +413,18 @@ const PaymentConfirmationPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
Scan QR code to transfer
|
Scan QR code to transfer
|
||||||
</p>
|
</p>
|
||||||
<img
|
{qrCodeUrl ? (
|
||||||
src={qrCodeUrl}
|
<img
|
||||||
alt="QR Code"
|
src={qrCodeUrl}
|
||||||
className="w-48 h-48 border-2
|
alt="QR Code"
|
||||||
border-gray-200 rounded-lg"
|
className="w-48 h-48 border-2
|
||||||
/>
|
border-gray-200 rounded-lg"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500 text-center">
|
||||||
|
Bank details not configured. Please contact support.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ const PaymentResultPage: React.FC = () => {
|
|||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
|
||||||
|
|
||||||
const supportEmail = settings.company_email || 'support@hotel.com';
|
const supportEmail = settings.company_email || null;
|
||||||
const supportPhone = settings.company_phone || '1900 xxxx';
|
const supportPhone = settings.company_phone || null;
|
||||||
|
|
||||||
const status = searchParams.get('status');
|
const status = searchParams.get('status');
|
||||||
const bookingId = searchParams.get('bookingId');
|
const bookingId = searchParams.get('bookingId');
|
||||||
|
|||||||
@@ -21,8 +21,14 @@ import LuxuryBookingModal from '../../features/bookings/components/LuxuryBooking
|
|||||||
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
|
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { logger } from '../../shared/utils/logger';
|
import { logger } from '../../shared/utils/logger';
|
||||||
|
import { useTheme } from '../../shared/contexts/ThemeContext';
|
||||||
|
import { getThemeBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../shared/utils/themeUtils';
|
||||||
|
|
||||||
const RoomDetailPage: React.FC = () => {
|
const RoomDetailPage: React.FC = () => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
const { room_number } = useParams<{ room_number: string }>();
|
const { room_number } = useParams<{ room_number: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { formatCurrency } = useFormatCurrency();
|
const { formatCurrency } = useFormatCurrency();
|
||||||
@@ -172,7 +178,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -184,9 +190,9 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="w-full px-2 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12">
|
<div className="w-full px-2 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12">
|
||||||
<div className="animate-pulse space-y-6">
|
<div className="animate-pulse space-y-6">
|
||||||
<div className="h-[600px] bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[var(--luxury-gold)]/20" />
|
<div className={`h-[600px] ${cardClasses} rounded-xl border border-[var(--luxury-gold)]/20`} />
|
||||||
<div className="h-12 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg w-1/3 border border-[var(--luxury-gold)]/10" />
|
<div className={`h-12 ${cardClasses} rounded-lg w-1/3 border border-[var(--luxury-gold)]/10`} />
|
||||||
<div className="h-6 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-lg w-2/3 border border-[var(--luxury-gold)]/10" />
|
<div className={`h-6 ${cardClasses} rounded-lg w-2/3 border border-[var(--luxury-gold)]/10`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +202,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
if (error || !room) {
|
if (error || !room) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -236,7 +242,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
className={`min-h-screen ${bgClasses} w-screen relative -mt-6 -mb-6`}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 'calc(50% - 50vw)',
|
marginLeft: 'calc(50% - 50vw)',
|
||||||
marginRight: 'calc(50% - 50vw)',
|
marginRight: 'calc(50% - 50vw)',
|
||||||
@@ -310,7 +316,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
: 'Maintenance'}
|
: 'Maintenance'}
|
||||||
</div>
|
</div>
|
||||||
{room.status === 'occupied' && bookedUntilDate && (
|
{room.status === 'occupied' && bookedUntilDate && (
|
||||||
<p className="text-[9px] sm:text-[10px] text-gray-300 font-light leading-tight">
|
<p className={`text-[9px] sm:text-[10px] ${textClasses.secondary} font-light leading-tight`}>
|
||||||
Booked until {bookedUntilDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
Booked until {bookedUntilDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -322,10 +328,12 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-xl sm:text-2xl lg:text-3xl font-serif font-semibold
|
<h1 className={`text-xl sm:text-2xl lg:text-3xl font-serif font-semibold
|
||||||
text-white mb-2 tracking-tight leading-tight
|
${textClasses.primary} mb-2 tracking-tight leading-tight
|
||||||
bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white
|
${theme.theme_layout_mode === 'light'
|
||||||
bg-clip-text text-transparent"
|
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||||
|
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'}
|
||||||
|
bg-clip-text text-transparent`}
|
||||||
>
|
>
|
||||||
{roomType?.name}
|
{roomType?.name}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -334,62 +342,62 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 sm:gap-3 mb-3">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 sm:gap-3 mb-3">
|
||||||
<div className="flex items-center gap-2
|
<div className={`flex items-center gap-2
|
||||||
p-2 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
p-2 ${cardClasses}
|
||||||
rounded-lg border border-[var(--luxury-gold)]/20
|
rounded-lg border border-[var(--luxury-gold)]/20
|
||||||
hover:border-[var(--luxury-gold)]/40 transition-all duration-300"
|
hover:border-[var(--luxury-gold)]/40 transition-all duration-300`}
|
||||||
>
|
>
|
||||||
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||||
border border-[var(--luxury-gold)]/30">
|
border border-[var(--luxury-gold)]/30">
|
||||||
<MapPin className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
<MapPin className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide mb-0.5">
|
<p className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide mb-0.5`}>
|
||||||
Location
|
Location
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs sm:text-sm text-white font-light tracking-wide">
|
<p className={`text-xs sm:text-sm ${textClasses.primary} font-light tracking-wide`}>
|
||||||
Room {room.room_number} - Floor {room.floor}
|
Room {room.room_number} - Floor {room.floor}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2
|
<div className={`flex items-center gap-2
|
||||||
p-2 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
p-2 ${cardClasses}
|
||||||
rounded-lg border border-[var(--luxury-gold)]/20"
|
rounded-lg border border-[var(--luxury-gold)]/20`}
|
||||||
>
|
>
|
||||||
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||||
border border-[var(--luxury-gold)]/30">
|
border border-[var(--luxury-gold)]/30">
|
||||||
<Users className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
<Users className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide mb-0.5">
|
<p className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide mb-0.5`}>
|
||||||
Capacity
|
Capacity
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs sm:text-sm text-white font-light tracking-wide">
|
<p className={`text-xs sm:text-sm ${textClasses.primary} font-light tracking-wide`}>
|
||||||
{room?.capacity || roomType?.capacity || 0} guests
|
{room?.capacity || roomType?.capacity || 0} guests
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{room.average_rating != null && (
|
{room.average_rating != null && (
|
||||||
<div className="flex items-center gap-2
|
<div className={`flex items-center gap-2
|
||||||
p-2 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
p-2 ${cardClasses}
|
||||||
rounded-lg border border-[var(--luxury-gold)]/20
|
rounded-lg border border-[var(--luxury-gold)]/20
|
||||||
hover:border-[var(--luxury-gold)]/40 transition-all duration-300"
|
hover:border-[var(--luxury-gold)]/40 transition-all duration-300`}
|
||||||
>
|
>
|
||||||
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||||
border border-[var(--luxury-gold)]/30">
|
border border-[var(--luxury-gold)]/30">
|
||||||
<Star className="w-3.5 h-3.5 text-[var(--luxury-gold)] fill-[var(--luxury-gold)]" />
|
<Star className="w-3.5 h-3.5 text-[var(--luxury-gold)] fill-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide mb-0.5">
|
<p className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide mb-0.5`}>
|
||||||
Rating
|
Rating
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<p className="text-xs sm:text-sm text-white font-semibold">
|
<p className={`text-xs sm:text-sm ${textClasses.primary} font-semibold`}>
|
||||||
{Number(room.average_rating).toFixed(1)}
|
{Number(room.average_rating).toFixed(1)}
|
||||||
</p>
|
</p>
|
||||||
<span className="text-[10px] sm:text-xs text-gray-500 font-light">
|
<span className={`text-[10px] sm:text-xs ${textClasses.muted} font-light`}>
|
||||||
({room.total_reviews || 0})
|
({room.total_reviews || 0})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,23 +409,23 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
{(room?.description || roomType?.description) && (
|
{(room?.description || roomType?.description) && (
|
||||||
<div className="p-3 sm:p-4 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
<div className={`p-3 sm:p-4 ${cardClasses}
|
||||||
rounded-lg border border-[var(--luxury-gold)]/20
|
rounded-lg border border-[var(--luxury-gold)]/20
|
||||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5"
|
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||||
border border-[var(--luxury-gold)]/30">
|
border border-[var(--luxury-gold)]/30">
|
||||||
<Award className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
<Award className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-sm sm:text-base font-serif font-semibold
|
<h2 className={`text-sm sm:text-base font-serif font-semibold
|
||||||
text-white tracking-wide"
|
${textClasses.primary} tracking-wide`}
|
||||||
>
|
>
|
||||||
{room?.description ? 'Room Description' : 'Room Type Description'}
|
{room?.description ? 'Room Description' : 'Room Type Description'}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-300 leading-relaxed
|
<p className={`${textClasses.secondary} leading-relaxed
|
||||||
font-light tracking-wide text-xs sm:text-sm"
|
font-light tracking-wide text-xs sm:text-sm`}
|
||||||
>
|
>
|
||||||
{room?.description || roomType?.description}
|
{room?.description || roomType?.description}
|
||||||
</p>
|
</p>
|
||||||
@@ -425,17 +433,17 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{}
|
{}
|
||||||
<div className="p-3 sm:p-4 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
<div className={`p-3 sm:p-4 ${cardClasses}
|
||||||
rounded-lg border border-[var(--luxury-gold)]/20
|
rounded-lg border border-[var(--luxury-gold)]/20
|
||||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5"
|
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
<div className="p-1 bg-[var(--luxury-gold)]/10 rounded-lg
|
||||||
border border-[var(--luxury-gold)]/30">
|
border border-[var(--luxury-gold)]/30">
|
||||||
<Sparkles className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
<Sparkles className="w-3.5 h-3.5 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-sm sm:text-base font-serif font-semibold
|
<h2 className={`text-sm sm:text-base font-serif font-semibold
|
||||||
text-white tracking-wide"
|
${textClasses.primary} tracking-wide`}
|
||||||
>
|
>
|
||||||
Amenities & Features
|
Amenities & Features
|
||||||
</h2>
|
</h2>
|
||||||
@@ -461,14 +469,14 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
<aside className="lg:col-span-4">
|
<aside className="lg:col-span-4">
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a]
|
<div className={`${cardClasses}
|
||||||
rounded-lg border border-[var(--luxury-gold)]/30
|
rounded-lg border border-[var(--luxury-gold)]/30
|
||||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/20
|
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/20
|
||||||
p-3 sm:p-4 sticky top-4"
|
p-3 sm:p-4 sticky top-4`}
|
||||||
>
|
>
|
||||||
{}
|
{}
|
||||||
<div className="mb-4 pb-4 border-b border-[var(--luxury-gold)]/20">
|
<div className="mb-4 pb-4 border-b border-[var(--luxury-gold)]/20">
|
||||||
<p className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide mb-1">
|
<p className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide mb-1`}>
|
||||||
Starting from
|
Starting from
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-baseline gap-1.5">
|
<div className="flex items-baseline gap-1.5">
|
||||||
@@ -480,7 +488,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{formattedPrice}
|
{formattedPrice}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide mt-0.5">
|
<div className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide mt-0.5`}>
|
||||||
/ night
|
/ night
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -529,7 +537,7 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
rounded-lg border border-[var(--luxury-gold)]/20 mb-3"
|
rounded-lg border border-[var(--luxury-gold)]/20 mb-3"
|
||||||
>
|
>
|
||||||
<Shield className="w-3.5 h-3.5 text-[var(--luxury-gold)] mt-0.5 flex-shrink-0" />
|
<Shield className="w-3.5 h-3.5 text-[var(--luxury-gold)] mt-0.5 flex-shrink-0" />
|
||||||
<p className="text-[10px] sm:text-xs text-gray-300 font-light tracking-wide leading-relaxed">
|
<p className={`text-[10px] sm:text-xs ${textClasses.secondary} font-light tracking-wide leading-relaxed`}>
|
||||||
<strong className="text-[var(--luxury-gold)]">20% deposit required</strong> to secure your booking. Pay the remaining balance on arrival at the hotel.
|
<strong className="text-[var(--luxury-gold)]">20% deposit required</strong> to secure your booking. Pay the remaining balance on arrival at the hotel.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -542,27 +550,27 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
<div className="flex items-center justify-between
|
<div className="flex items-center justify-between
|
||||||
py-1.5 border-b border-[var(--luxury-gold)]/10"
|
py-1.5 border-b border-[var(--luxury-gold)]/10"
|
||||||
>
|
>
|
||||||
<span className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide">Room Type</span>
|
<span className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide`}>Room Type</span>
|
||||||
<strong className="text-xs sm:text-sm text-white font-light">{roomType?.name}</strong>
|
<strong className={`text-xs sm:text-sm ${textClasses.primary} font-light`}>{roomType?.name}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between
|
<div className="flex items-center justify-between
|
||||||
py-1.5 border-b border-[var(--luxury-gold)]/10"
|
py-1.5 border-b border-[var(--luxury-gold)]/10"
|
||||||
>
|
>
|
||||||
<span className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide">Max Guests</span>
|
<span className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide`}>Max Guests</span>
|
||||||
<span className="text-xs sm:text-sm text-white font-light">{(room?.capacity || roomType?.capacity || 0)} guests</span>
|
<span className={`text-xs sm:text-sm ${textClasses.primary} font-light`}>{(room?.capacity || roomType?.capacity || 0)} guests</span>
|
||||||
</div>
|
</div>
|
||||||
{room?.room_size && (
|
{room?.room_size && (
|
||||||
<div className="flex items-center justify-between
|
<div className="flex items-center justify-between
|
||||||
py-1.5 border-b border-[var(--luxury-gold)]/10"
|
py-1.5 border-b border-[var(--luxury-gold)]/10"
|
||||||
>
|
>
|
||||||
<span className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide">Room Size</span>
|
<span className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide`}>Room Size</span>
|
||||||
<span className="text-xs sm:text-sm text-white font-light">{room.room_size}</span>
|
<span className={`text-xs sm:text-sm ${textClasses.primary} font-light`}>{room.room_size}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{room?.view && (
|
{room?.view && (
|
||||||
<div className={`flex items-center justify-between ${room?.room_size ? 'py-1.5 border-b border-[var(--luxury-gold)]/10' : 'py-1.5'}`}>
|
<div className={`flex items-center justify-between ${room?.room_size ? 'py-1.5 border-b border-[var(--luxury-gold)]/10' : 'py-1.5'}`}>
|
||||||
<span className="text-[10px] sm:text-xs text-gray-400 font-light tracking-wide">View</span>
|
<span className={`text-[10px] sm:text-xs ${textClasses.muted} font-light tracking-wide`}>View</span>
|
||||||
<span className="text-xs sm:text-sm text-white font-light">{room.view}</span>
|
<span className={`text-xs sm:text-sm ${textClasses.primary} font-light`}>{room.view}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -571,9 +579,9 @@ const RoomDetailPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
{}
|
||||||
<div className="mb-4 p-3 sm:p-4 bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
<div className={`mb-4 p-3 sm:p-4 ${cardClasses}
|
||||||
rounded-lg border border-[var(--luxury-gold)]/20
|
rounded-lg border border-[var(--luxury-gold)]/20
|
||||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5"
|
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/5`}
|
||||||
>
|
>
|
||||||
<ReviewSection roomId={room.id} />
|
<ReviewSection roomId={room.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,8 +9,15 @@ import Pagination from '../../shared/components/Pagination';
|
|||||||
import { ArrowLeft, Hotel, Filter, ChevronDown, ChevronUp, Tag, X, CheckCircle } from 'lucide-react';
|
import { ArrowLeft, Hotel, Filter, ChevronDown, ChevronUp, Tag, X, CheckCircle } from 'lucide-react';
|
||||||
import { logger } from '../../shared/utils/logger';
|
import { logger } from '../../shared/utils/logger';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { useTheme } from '../../shared/contexts/ThemeContext';
|
||||||
|
import { getThemeBackgroundClasses, getThemeHeroBackgroundClasses, getThemeTextClasses, getThemeCardClasses } from '../../shared/utils/themeUtils';
|
||||||
|
|
||||||
const RoomListPage: React.FC = () => {
|
const RoomListPage: React.FC = () => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const bgClasses = getThemeBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const heroBgClasses = getThemeHeroBackgroundClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [rooms, setRooms] = useState<Room[]>([]);
|
const [rooms, setRooms] = useState<Room[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -146,13 +153,13 @@ const RoomListPage: React.FC = () => {
|
|||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-full" style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
<div className={`min-h-screen ${bgClasses} w-full`} style={{ width: '100vw', position: 'relative', left: '50%', right: '50%', marginLeft: '-50vw', marginRight: '-50vw', marginTop: '-1.5rem', marginBottom: '-1.5rem' }}>
|
||||||
{}
|
{}
|
||||||
{/* Promotion Banner */}
|
{/* Promotion Banner */}
|
||||||
{showPromotionBanner && activePromotion && (
|
{showPromotionBanner && activePromotion && (
|
||||||
<div className="w-full bg-gradient-to-r from-[var(--luxury-gold)]/20 via-[var(--luxury-gold-light)]/15 to-[var(--luxury-gold)]/20 border-b border-[var(--luxury-gold)]/30">
|
<div className="w-full bg-gradient-to-r from-[var(--luxury-gold)]/20 via-[var(--luxury-gold-light)]/15 to-[var(--luxury-gold)]/20 border-b border-[var(--luxury-gold)]/30">
|
||||||
<div className="w-full max-w-[1920px] mx-auto px-3 sm:px-4 md:px-6 lg:px-8 xl:px-12 2xl:px-16 3xl:px-20 py-4">
|
<div className="w-full max-w-[1920px] mx-auto px-3 sm:px-4 md:px-6 lg:px-8 xl:px-12 2xl:px-16 3xl:px-20 py-4">
|
||||||
<div className="flex items-center justify-between gap-4 bg-gradient-to-r from-[#1a1a1a] to-[#0f0f0f] border border-[var(--luxury-gold)]/40 rounded-lg p-4 backdrop-blur-xl shadow-lg">
|
<div className={`flex items-center justify-between gap-4 ${cardClasses} border border-[var(--luxury-gold)]/40 rounded-lg p-4 backdrop-blur-xl shadow-lg`}>
|
||||||
<div className="flex items-center gap-3 flex-1">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
<div className="p-2 bg-[var(--luxury-gold)]/20 rounded-lg border border-[var(--luxury-gold)]/40">
|
<div className="p-2 bg-[var(--luxury-gold)]/20 rounded-lg border border-[var(--luxury-gold)]/40">
|
||||||
<Tag className="w-5 h-5 text-[var(--luxury-gold)]" />
|
<Tag className="w-5 h-5 text-[var(--luxury-gold)]" />
|
||||||
@@ -165,11 +172,11 @@ const RoomListPage: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{activePromotion.discount && (
|
{activePromotion.discount && (
|
||||||
<p className="text-xs text-gray-300">
|
<p className={`text-xs ${textClasses.secondary}`}>
|
||||||
{activePromotion.discount} - {activePromotion.description || 'Valid on bookings'}
|
{activePromotion.discount} - {activePromotion.description || 'Valid on bookings'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-gray-400 mt-1">
|
<p className={`text-xs ${textClasses.muted} mt-1`}>
|
||||||
The promotion code will be automatically applied when you book a room
|
The promotion code will be automatically applied when you book a room
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,14 +186,14 @@ const RoomListPage: React.FC = () => {
|
|||||||
className="p-2 hover:bg-[var(--luxury-gold)]/10 rounded-lg transition-colors"
|
className="p-2 hover:bg-[var(--luxury-gold)]/10 rounded-lg transition-colors"
|
||||||
aria-label="Dismiss promotion"
|
aria-label="Dismiss promotion"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5 text-gray-400 hover:text-white" />
|
<X className={`w-5 h-5 ${textClasses.muted} ${theme.theme_layout_mode === 'light' ? 'hover:text-gray-900' : 'hover:text-white'}`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8">
|
<div className={`w-full ${heroBgClasses} border-b border-[var(--luxury-gold)]/10 pt-6 sm:pt-7 md:pt-8`}>
|
||||||
<div className="w-full max-w-[1920px] mx-auto px-3 sm:px-4 md:px-6 lg:px-8 xl:px-12 2xl:px-16 3xl:px-20 py-6 sm:py-7 md:py-8 lg:py-10">
|
<div className="w-full max-w-[1920px] mx-auto px-3 sm:px-4 md:px-6 lg:px-8 xl:px-12 2xl:px-16 3xl:px-20 py-6 sm:py-7 md:py-8 lg:py-10">
|
||||||
{}
|
{}
|
||||||
<Link
|
<Link
|
||||||
@@ -212,14 +219,16 @@ const RoomListPage: React.FC = () => {
|
|||||||
<Hotel className="w-5 h-5 sm:w-5 sm:h-5 text-[var(--luxury-gold)]" />
|
<Hotel className="w-5 h-5 sm:w-5 sm:h-5 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold
|
<h1 className={`text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold
|
||||||
text-white mb-2 sm:mb-3 tracking-tight leading-tight px-2
|
${textClasses.primary} mb-2 sm:mb-3 tracking-tight leading-tight px-2
|
||||||
bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white
|
${theme.theme_layout_mode === 'light'
|
||||||
bg-clip-text text-transparent"
|
? 'bg-gradient-to-r from-gray-900 via-[var(--luxury-gold)] to-gray-900'
|
||||||
|
: 'bg-gradient-to-r from-white via-[var(--luxury-gold)] to-white'}
|
||||||
|
bg-clip-text text-transparent`}
|
||||||
>
|
>
|
||||||
Our Rooms & Suites
|
Our Rooms & Suites
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-400 font-light tracking-wide text-xs sm:text-sm md:text-base max-w-xl mx-auto px-2 sm:px-4 leading-relaxed">
|
<p className={`${textClasses.muted} font-light tracking-wide text-xs sm:text-sm md:text-base max-w-xl mx-auto px-2 sm:px-4 leading-relaxed`}>
|
||||||
Discover our collection of luxurious accommodations,
|
Discover our collection of luxurious accommodations,
|
||||||
each designed to provide an exceptional stay
|
each designed to provide an exceptional stay
|
||||||
</p>
|
</p>
|
||||||
@@ -235,18 +244,18 @@ const RoomListPage: React.FC = () => {
|
|||||||
<div className="xl:hidden order-1 mb-4">
|
<div className="xl:hidden order-1 mb-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsFilterOpen(!isFilterOpen)}
|
onClick={() => setIsFilterOpen(!isFilterOpen)}
|
||||||
className="w-full bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
className={`w-full ${cardClasses}
|
||||||
border border-[var(--luxury-gold)]/30 rounded-xl p-4
|
border border-[var(--luxury-gold)]/30 rounded-xl p-4
|
||||||
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/10
|
backdrop-blur-xl shadow-lg shadow-[var(--luxury-gold)]/10
|
||||||
flex items-center justify-between gap-3
|
flex items-center justify-between gap-3
|
||||||
hover:border-[var(--luxury-gold)]/50 hover:shadow-xl hover:shadow-[var(--luxury-gold)]/20
|
hover:border-[var(--luxury-gold)]/50 hover:shadow-xl hover:shadow-[var(--luxury-gold)]/20
|
||||||
transition-all duration-300 touch-manipulation"
|
transition-all duration-300 touch-manipulation`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-[var(--luxury-gold)]/10 rounded-lg border border-[var(--luxury-gold)]/30">
|
<div className="p-2 bg-[var(--luxury-gold)]/10 rounded-lg border border-[var(--luxury-gold)]/30">
|
||||||
<Filter className="w-5 h-5 text-[var(--luxury-gold)]" />
|
<Filter className="w-5 h-5 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-medium tracking-wide text-base">
|
<span className={`${textClasses.primary} font-medium tracking-wide text-base`}>
|
||||||
Filters
|
Filters
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -320,21 +329,21 @@ const RoomListPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !error && rooms.length === 0 && (
|
{!loading && !error && rooms.length === 0 && (
|
||||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a]
|
<div className={`${cardClasses}
|
||||||
border border-[var(--luxury-gold)]/20 rounded-xl p-8 sm:p-10 md:p-12 lg:p-16 text-center
|
border border-[var(--luxury-gold)]/20 rounded-xl p-8 sm:p-10 md:p-12 lg:p-16 text-center
|
||||||
backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5"
|
backdrop-blur-xl shadow-2xl shadow-[var(--luxury-gold)]/5`}
|
||||||
>
|
>
|
||||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24
|
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24
|
||||||
bg-[var(--luxury-gold)]/10 rounded-2xl mb-4 sm:mb-5 md:mb-6 lg:mb-8 border border-[var(--luxury-gold)]/30"
|
bg-[var(--luxury-gold)]/10 rounded-2xl mb-4 sm:mb-5 md:mb-6 lg:mb-8 border border-[var(--luxury-gold)]/30"
|
||||||
>
|
>
|
||||||
<Hotel className="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 text-[var(--luxury-gold)]" />
|
<Hotel className="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 text-[var(--luxury-gold)]" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg sm:text-xl md:text-2xl font-serif font-semibold
|
<h3 className={`text-lg sm:text-xl md:text-2xl font-serif font-semibold
|
||||||
text-white mb-3 sm:mb-4 tracking-wide px-2"
|
${textClasses.primary} mb-3 sm:mb-4 tracking-wide px-2`}
|
||||||
>
|
>
|
||||||
No matching rooms found
|
No matching rooms found
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-400 font-light tracking-wide mb-5 sm:mb-6 md:mb-8 text-sm sm:text-base md:text-lg px-2">
|
<p className={`${textClasses.muted} font-light tracking-wide mb-5 sm:mb-6 md:mb-8 text-sm sm:text-base md:text-lg px-2`}>
|
||||||
Please try adjusting the filters or search differently
|
Please try adjusting the filters or search differently
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
@@ -354,7 +363,7 @@ const RoomListPage: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
{}
|
{}
|
||||||
<div className="mb-3 sm:mb-4 md:mb-5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-3">
|
<div className="mb-3 sm:mb-4 md:mb-5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-3">
|
||||||
<p className="text-gray-400 font-light tracking-wide text-xs sm:text-sm md:text-base">
|
<p className={`${textClasses.muted} font-light tracking-wide text-xs sm:text-sm md:text-base`}>
|
||||||
Showing <span className="text-[var(--luxury-gold)] font-medium">{rooms.length}</span> of{' '}
|
Showing <span className="text-[var(--luxury-gold)] font-medium">{rooms.length}</span> of{' '}
|
||||||
<span className="text-[var(--luxury-gold)] font-medium">{pagination.total}</span> rooms
|
<span className="text-[var(--luxury-gold)] font-medium">{pagination.total}</span> rooms
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const CurrencyIcon: React.FC<CurrencyIconProps> = ({
|
|||||||
currency
|
currency
|
||||||
}) => {
|
}) => {
|
||||||
const { currency: contextCurrency } = useCurrency();
|
const { currency: contextCurrency } = useCurrency();
|
||||||
const currencyToUse = currency || contextCurrency || 'VND';
|
const currencyToUse = currency || contextCurrency || 'USD';
|
||||||
const symbol = getCurrencySymbol(currencyToUse);
|
const symbol = getCurrencySymbol(currencyToUse);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const CurrencySelector: React.FC<CurrencySelectorProps> = ({
|
|||||||
const isAdmin = userInfo?.role === 'admin';
|
const isAdmin = userInfo?.role === 'admin';
|
||||||
|
|
||||||
const currencyNames: Record<string, string> = {
|
const currencyNames: Record<string, string> = {
|
||||||
VND: 'Vietnamese Dong',
|
|
||||||
USD: 'US Dollar',
|
USD: 'US Dollar',
|
||||||
EUR: 'Euro',
|
EUR: 'Euro',
|
||||||
GBP: 'British Pound',
|
GBP: 'British Pound',
|
||||||
|
|||||||
@@ -29,11 +29,18 @@ import CookiePreferencesLink from './CookiePreferencesLink';
|
|||||||
import ChatWidget from '../../features/notifications/components/ChatWidget';
|
import ChatWidget from '../../features/notifications/components/ChatWidget';
|
||||||
import pageContentService, { type PageContent } from '../../features/content/services/pageContentService';
|
import pageContentService, { type PageContent } from '../../features/content/services/pageContentService';
|
||||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import apiClient from '../services/apiClient';
|
import apiClient from '../services/apiClient';
|
||||||
|
import { formatWorkingHours } from '../utils/format';
|
||||||
|
import { Clock } from 'lucide-react';
|
||||||
|
import { getThemeTextClasses, getThemeCardClasses } from '../utils/themeUtils';
|
||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
const cardClasses = getThemeCardClasses(theme.theme_layout_mode);
|
||||||
const [homePageContent, setHomePageContent] = useState<PageContent | null>(null);
|
const [homePageContent, setHomePageContent] = useState<PageContent | null>(null);
|
||||||
const [enabledPages, setEnabledPages] = useState<Set<string>>(new Set());
|
const [enabledPages, setEnabledPages] = useState<Set<string>>(new Set());
|
||||||
const [apiError, setApiError] = useState(false);
|
const [apiError, setApiError] = useState(false);
|
||||||
@@ -192,7 +199,9 @@ const Footer: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="relative bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black text-gray-300 overflow-hidden">
|
<footer className={`relative ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-b from-gray-50 via-white to-gray-100'
|
||||||
|
: 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black'} ${textClasses.secondary} overflow-hidden`}>
|
||||||
{/* Top Gold Accent Line */}
|
{/* Top Gold Accent Line */}
|
||||||
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent shadow-lg shadow-[var(--luxury-gold)]/50"></div>
|
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent shadow-lg shadow-[var(--luxury-gold)]/50"></div>
|
||||||
|
|
||||||
@@ -228,7 +237,7 @@ const Footer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl sm:text-3xl font-display font-semibold text-white tracking-tight mb-1">
|
<h2 className={`text-2xl sm:text-3xl font-display font-semibold ${textClasses.primary} tracking-tight mb-1`}>
|
||||||
{settings.company_name || pageContent?.title || 'Luxury Hotel'}
|
{settings.company_name || pageContent?.title || 'Luxury Hotel'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs sm:text-sm text-[var(--luxury-gold)] font-light tracking-[3px] sm:tracking-[4px] uppercase">
|
<p className="text-xs sm:text-sm text-[var(--luxury-gold)] font-light tracking-[3px] sm:tracking-[4px] uppercase">
|
||||||
@@ -236,7 +245,7 @@ const Footer: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm sm:text-base text-gray-400 mb-8 leading-relaxed max-w-md font-light">
|
<p className={`text-sm sm:text-base ${textClasses.muted} mb-8 leading-relaxed max-w-md font-light`}>
|
||||||
{pageContent?.description || 'Experience unparalleled luxury and world-class hospitality. Your journey to exceptional comfort begins here.'}
|
{pageContent?.description || 'Experience unparalleled luxury and world-class hospitality. Your journey to exceptional comfort begins here.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -252,7 +261,7 @@ const Footer: React.FC = () => {
|
|||||||
className="group flex items-center space-x-2 px-3 py-2 bg-gradient-to-r from-[var(--luxury-gold)]/5 to-transparent border border-[var(--luxury-gold)]/10 rounded-lg hover:border-[var(--luxury-gold)]/30 hover:from-[var(--luxury-gold)]/10 transition-all duration-300"
|
className="group flex items-center space-x-2 px-3 py-2 bg-gradient-to-r from-[var(--luxury-gold)]/5 to-transparent border border-[var(--luxury-gold)]/10 rounded-lg hover:border-[var(--luxury-gold)]/30 hover:from-[var(--luxury-gold)]/10 transition-all duration-300"
|
||||||
>
|
>
|
||||||
<BadgeIcon className="w-4 h-4 sm:w-5 sm:h-5 text-[var(--luxury-gold)] group-hover:scale-110 transition-transform duration-300" />
|
<BadgeIcon className="w-4 h-4 sm:w-5 sm:h-5 text-[var(--luxury-gold)] group-hover:scale-110 transition-transform duration-300" />
|
||||||
<span className="text-xs sm:text-sm font-medium tracking-wide text-gray-300 group-hover:text-[var(--luxury-gold)] transition-colors">{badge.text}</span>
|
<span className={`text-xs sm:text-sm font-medium tracking-wide ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-colors`}>{badge.text}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -266,10 +275,12 @@ const Footer: React.FC = () => {
|
|||||||
href={pageContent.social_links.facebook}
|
href={pageContent.social_links.facebook}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
|
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-white border border-gray-300 shadow-sm'
|
||||||
|
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
|
||||||
aria-label="Facebook"
|
aria-label="Facebook"
|
||||||
>
|
>
|
||||||
<Facebook className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
<Facebook className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
|
||||||
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -278,10 +289,12 @@ const Footer: React.FC = () => {
|
|||||||
href={pageContent.social_links.twitter}
|
href={pageContent.social_links.twitter}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
|
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-white border border-gray-300 shadow-sm'
|
||||||
|
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
|
||||||
aria-label="Twitter"
|
aria-label="Twitter"
|
||||||
>
|
>
|
||||||
<Twitter className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
<Twitter className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
|
||||||
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -290,10 +303,12 @@ const Footer: React.FC = () => {
|
|||||||
href={pageContent.social_links.instagram}
|
href={pageContent.social_links.instagram}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
|
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-white border border-gray-300 shadow-sm'
|
||||||
|
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
|
||||||
aria-label="Instagram"
|
aria-label="Instagram"
|
||||||
>
|
>
|
||||||
<Instagram className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
<Instagram className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
|
||||||
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -302,10 +317,12 @@ const Footer: React.FC = () => {
|
|||||||
href={pageContent.social_links.linkedin}
|
href={pageContent.social_links.linkedin}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
|
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-white border border-gray-300 shadow-sm'
|
||||||
|
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
|
||||||
aria-label="LinkedIn"
|
aria-label="LinkedIn"
|
||||||
>
|
>
|
||||||
<Linkedin className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
<Linkedin className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
|
||||||
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -314,10 +331,12 @@ const Footer: React.FC = () => {
|
|||||||
href={pageContent.social_links.youtube}
|
href={pageContent.social_links.youtube}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50 hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5"
|
className={`group relative w-12 h-12 sm:w-14 sm:h-14 flex items-center justify-center rounded-lg ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-white border border-gray-300 shadow-sm'
|
||||||
|
: 'bg-gradient-to-br from-gray-800/60 to-gray-900/60 backdrop-blur-sm border border-gray-700/50'} hover:border-[var(--luxury-gold)]/60 transition-all duration-300 hover:bg-gradient-to-br hover:from-[var(--luxury-gold)]/10 hover:to-[var(--luxury-gold-dark)]/10 hover:shadow-lg hover:shadow-[var(--luxury-gold)]/20 hover:-translate-y-0.5`}
|
||||||
aria-label="YouTube"
|
aria-label="YouTube"
|
||||||
>
|
>
|
||||||
<Youtube className="w-5 h-5 sm:w-6 sm:h-6 text-gray-400 group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
<Youtube className={`w-5 h-5 sm:w-6 sm:h-6 ${textClasses.secondary} group-hover:text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110`} />
|
||||||
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
<div className="absolute inset-0 rounded-lg bg-[var(--luxury-gold)]/0 group-hover:bg-[var(--luxury-gold)]/10 blur-xl transition-all duration-500"></div>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -327,7 +346,7 @@ const Footer: React.FC = () => {
|
|||||||
{/* Quick Links */}
|
{/* Quick Links */}
|
||||||
{quickLinks.length > 0 && (
|
{quickLinks.length > 0 && (
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide">
|
<h3 className={`${textClasses.primary} font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide`}>
|
||||||
<span className="relative z-10">Quick Links</span>
|
<span className="relative z-10">Quick Links</span>
|
||||||
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
||||||
</h3>
|
</h3>
|
||||||
@@ -336,7 +355,7 @@ const Footer: React.FC = () => {
|
|||||||
<li key={link.url}>
|
<li key={link.url}>
|
||||||
<Link
|
<Link
|
||||||
to={link.url}
|
to={link.url}
|
||||||
className="group flex items-center text-sm sm:text-base text-gray-400 hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide"
|
className={`group flex items-center text-sm sm:text-base ${textClasses.muted} hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide`}
|
||||||
>
|
>
|
||||||
<span className="absolute left-0 w-0 h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] group-hover:w-8 transition-all duration-300 rounded-full"></span>
|
<span className="absolute left-0 w-0 h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] group-hover:w-8 transition-all duration-300 rounded-full"></span>
|
||||||
<span className="ml-10 group-hover:translate-x-2 transition-transform duration-300 group-hover:font-medium">{link.label}</span>
|
<span className="ml-10 group-hover:translate-x-2 transition-transform duration-300 group-hover:font-medium">{link.label}</span>
|
||||||
@@ -350,7 +369,7 @@ const Footer: React.FC = () => {
|
|||||||
{/* Guest Services */}
|
{/* Guest Services */}
|
||||||
{supportLinks.length > 0 && (
|
{supportLinks.length > 0 && (
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide">
|
<h3 className={`${textClasses.primary} font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide`}>
|
||||||
<span className="relative z-10">Guest Services</span>
|
<span className="relative z-10">Guest Services</span>
|
||||||
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
||||||
</h3>
|
</h3>
|
||||||
@@ -359,7 +378,7 @@ const Footer: React.FC = () => {
|
|||||||
<li key={link.url}>
|
<li key={link.url}>
|
||||||
<Link
|
<Link
|
||||||
to={link.url}
|
to={link.url}
|
||||||
className="group flex items-center text-sm sm:text-base text-gray-400 hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide"
|
className={`group flex items-center text-sm sm:text-base ${textClasses.muted} hover:text-[var(--luxury-gold)] transition-all duration-300 relative font-light tracking-wide`}
|
||||||
>
|
>
|
||||||
<span className="absolute left-0 w-0 h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] group-hover:w-8 transition-all duration-300 rounded-full"></span>
|
<span className="absolute left-0 w-0 h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] group-hover:w-8 transition-all duration-300 rounded-full"></span>
|
||||||
<span className="ml-10 group-hover:translate-x-2 transition-transform duration-300 group-hover:font-medium">{link.label}</span>
|
<span className="ml-10 group-hover:translate-x-2 transition-transform duration-300 group-hover:font-medium">{link.label}</span>
|
||||||
@@ -377,7 +396,7 @@ const Footer: React.FC = () => {
|
|||||||
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
||||||
</h3>
|
</h3>
|
||||||
{homePageContent?.newsletter_section_subtitle && (
|
{homePageContent?.newsletter_section_subtitle && (
|
||||||
<p className="text-sm text-gray-400 mb-4 font-light leading-relaxed">
|
<p className={`text-sm ${textClasses.muted} mb-4 font-light leading-relaxed`}>
|
||||||
{homePageContent.newsletter_section_subtitle}
|
{homePageContent.newsletter_section_subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -416,7 +435,9 @@ const Footer: React.FC = () => {
|
|||||||
value={newsletterEmail}
|
value={newsletterEmail}
|
||||||
onChange={(e) => setNewsletterEmail(e.target.value)}
|
onChange={(e) => setNewsletterEmail(e.target.value)}
|
||||||
placeholder={homePageContent?.newsletter_placeholder || 'Enter your email'}
|
placeholder={homePageContent?.newsletter_placeholder || 'Enter your email'}
|
||||||
className="w-full px-4 py-2.5 rounded-lg border border-gray-700 bg-gray-800/50 text-white placeholder-gray-400 focus:border-[var(--luxury-gold)] focus:ring-2 focus:ring-[var(--luxury-gold)]/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
className={`w-full px-4 py-2.5 rounded-lg border ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'border-gray-300 bg-white text-gray-900 placeholder-gray-500'
|
||||||
|
: 'border-gray-700 bg-gray-800/50 text-white placeholder-gray-400'} focus:border-[var(--luxury-gold)] focus:ring-2 focus:ring-[var(--luxury-gold)]/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm`}
|
||||||
required
|
required
|
||||||
disabled={newsletterSubmitting}
|
disabled={newsletterSubmitting}
|
||||||
/>
|
/>
|
||||||
@@ -441,7 +462,7 @@ const Footer: React.FC = () => {
|
|||||||
|
|
||||||
{/* Contact Information */}
|
{/* Contact Information */}
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h3 className="text-white font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide">
|
<h3 className={`${textClasses.primary} font-elegant font-semibold text-lg sm:text-xl mb-4 sm:mb-6 relative inline-block tracking-wide`}>
|
||||||
<span className="relative z-10">Contact</span>
|
<span className="relative z-10">Contact</span>
|
||||||
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
<span className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--luxury-gold)] via-[var(--luxury-gold)]/50 to-transparent"></span>
|
||||||
</h3>
|
</h3>
|
||||||
@@ -455,7 +476,7 @@ const Footer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm sm:text-base text-gray-400 group-hover:text-gray-300 transition-colors leading-relaxed font-light pt-1">
|
<span className={`text-sm sm:text-base ${textClasses.muted} group-hover:${textClasses.secondary} transition-colors leading-relaxed font-light pt-1`}>
|
||||||
{displayAddress
|
{displayAddress
|
||||||
.split('\n').map((line, i) => (
|
.split('\n').map((line, i) => (
|
||||||
<React.Fragment key={i}>
|
<React.Fragment key={i}>
|
||||||
@@ -474,7 +495,7 @@ const Footer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<a href={phoneHref} className="text-sm sm:text-base text-gray-400 group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide">
|
<a href={phoneHref} className={`text-sm sm:text-base ${textClasses.muted} group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide`}>
|
||||||
{displayPhone}
|
{displayPhone}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -487,11 +508,29 @@ const Footer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
</div>
|
</div>
|
||||||
<a href={`mailto:${displayEmail}`} className="text-sm sm:text-base text-gray-400 group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide break-all">
|
<a href={`mailto:${displayEmail}`} className={`text-sm sm:text-base ${textClasses.muted} group-hover:text-[var(--luxury-gold)] transition-colors font-light tracking-wide break-all`}>
|
||||||
{displayEmail}
|
{displayEmail}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
|
||||||
|
<li className="flex items-start space-x-4 group">
|
||||||
|
<div className="relative mt-1 flex-shrink-0">
|
||||||
|
<div className="p-2 bg-gradient-to-br from-[var(--luxury-gold)]/10 to-[var(--luxury-gold-dark)]/5 rounded-lg border border-[var(--luxury-gold)]/20 group-hover:border-[var(--luxury-gold)]/40 transition-all duration-300">
|
||||||
|
<Clock className="w-4 h-4 sm:w-5 sm:h-5 text-[var(--luxury-gold)] transition-all duration-300 group-hover:scale-110" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 bg-[var(--luxury-gold)]/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
|
</div>
|
||||||
|
<div className="pt-1">
|
||||||
|
<span className={`text-sm sm:text-base ${textClasses.muted} group-hover:${textClasses.secondary} transition-colors font-light tracking-wide block`}>
|
||||||
|
Chat Support Hours
|
||||||
|
</span>
|
||||||
|
<span className="text-xs sm:text-sm text-gray-500 font-light">
|
||||||
|
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useClickOutside } from '../hooks/useClickOutside';
|
import { useClickOutside } from '../hooks/useClickOutside';
|
||||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
|
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
|
||||||
import { normalizeImageUrl } from '../utils/imageUtils';
|
import { normalizeImageUrl } from '../utils/imageUtils';
|
||||||
import InAppNotificationBell from '../../features/notifications/components/InAppNotificationBell';
|
import InAppNotificationBell from '../../features/notifications/components/InAppNotificationBell';
|
||||||
import Navbar from './Navbar';
|
import Navbar from './Navbar';
|
||||||
|
import { getThemeTextClasses } from '../utils/themeUtils';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
isAuthenticated?: boolean;
|
isAuthenticated?: boolean;
|
||||||
@@ -40,12 +42,14 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onLogout
|
onLogout
|
||||||
}) => {
|
}) => {
|
||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const { openModal } = useAuthModal();
|
const { openModal } = useAuthModal();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
|
|
||||||
|
|
||||||
const displayPhone = settings.company_phone || '+1 (234) 567-890';
|
const displayPhone = settings.company_phone || null;
|
||||||
const displayEmail = settings.company_email || 'info@luxuryhotel.com';
|
const displayEmail = settings.company_email || null;
|
||||||
const logoUrl = settings.company_logo_url
|
const logoUrl = settings.company_logo_url
|
||||||
? (settings.company_logo_url.startsWith('http')
|
? (settings.company_logo_url.startsWith('http')
|
||||||
? settings.company_logo_url
|
? settings.company_logo_url
|
||||||
@@ -110,12 +114,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3.5 text-white/95
|
space-x-2 px-4 py-3.5 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-md transition-all duration-300
|
rounded-md transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wider text-sm w-full text-left group relative mx-2 cursor-pointer"
|
hover:border-[var(--luxury-gold)] font-light tracking-wider text-sm w-full text-left group relative mx-2 cursor-pointer`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<LogIn className="w-4 h-4" />
|
<LogIn className="w-4 h-4" />
|
||||||
@@ -160,7 +164,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className="flex items-center
|
||||||
space-x-2 px-4 py-3.5 text-white/95
|
space-x-2 px-4 py-3.5 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-md transition-all duration-300
|
rounded-md transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
@@ -181,12 +185,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
@@ -201,12 +205,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<Heart className="w-4 h-4" />
|
<Heart className="w-4 h-4" />
|
||||||
@@ -221,12 +225,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
@@ -241,12 +245,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<Star className="w-4 h-4" />
|
<Star className="w-4 h-4" />
|
||||||
@@ -261,12 +265,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<Users className="w-4 h-4" />
|
<Users className="w-4 h-4" />
|
||||||
@@ -281,12 +285,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<AlertCircle className="w-4 h-4" />
|
<AlertCircle className="w-4 h-4" />
|
||||||
@@ -301,12 +305,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<Bell className="w-4 h-4" />
|
<Bell className="w-4 h-4" />
|
||||||
@@ -321,12 +325,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<Shield className="w-4 h-4" />
|
<Shield className="w-4 h-4" />
|
||||||
@@ -344,12 +348,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
@@ -366,12 +370,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
@@ -388,12 +392,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
@@ -410,12 +414,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onTouchStart={(e) => {
|
onTouchStart={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
style={{ touchAction: 'manipulation' }}
|
style={{ touchAction: 'manipulation' }}
|
||||||
>
|
>
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
@@ -446,9 +450,15 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className="bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] sticky top-0 z-50 border-b border-[rgba(var(--luxury-gold-rgb),0.15)] shadow-[0_8px_32px_rgba(0,0,0,0.4)] backdrop-blur-sm"
|
className={`sticky top-0 z-50 border-b backdrop-blur-sm ${
|
||||||
|
theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-b from-white via-gray-50 to-white border-gray-200 shadow-[0_8px_32px_rgba(0,0,0,0.1)]'
|
||||||
|
: 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] border-[rgba(var(--luxury-gold-rgb),0.15)] shadow-[0_8px_32px_rgba(0,0,0,0.4)]'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="hidden lg:block bg-[#0a0a0a]/50 border-b border-[rgba(var(--luxury-gold-rgb),0.1)]">
|
<div className={`hidden lg:block ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gray-50/80 border-gray-200'
|
||||||
|
: 'bg-[#0a0a0a]/50 border-[rgba(var(--luxury-gold-rgb),0.1)]'} border-b`}>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
|
||||||
<div className="flex items-center justify-end space-x-6 text-sm">
|
<div className="flex items-center justify-end space-x-6 text-sm">
|
||||||
{displayPhone && (
|
{displayPhone && (
|
||||||
@@ -492,7 +502,9 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col min-w-0">
|
<div className="flex flex-col min-w-0">
|
||||||
<span className="text-sm sm:text-base md:text-lg lg:text-xl xl:text-2xl font-display font-semibold text-white tracking-tight leading-tight bg-gradient-to-r from-white to-white/90 bg-clip-text truncate">
|
<span className={`text-sm sm:text-base md:text-lg lg:text-xl xl:text-2xl font-display font-semibold ${textClasses.primary} tracking-tight leading-tight ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-r from-gray-900 to-gray-700 bg-clip-text'
|
||||||
|
: 'bg-gradient-to-r from-white to-white/90 bg-clip-text'} truncate`}>
|
||||||
{settings.company_name}
|
{settings.company_name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[8px] sm:text-[9px] md:text-[10px] text-[var(--luxury-gold)] tracking-[0.25em] uppercase font-light hidden sm:block">
|
<span className="text-[8px] sm:text-[9px] md:text-[10px] text-[var(--luxury-gold)] tracking-[0.25em] uppercase font-light hidden sm:block">
|
||||||
@@ -530,10 +542,10 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => openModal('login')}
|
onClick={() => openModal('login')}
|
||||||
className="flex items-center space-x-2
|
className={`flex items-center space-x-2
|
||||||
px-6 py-2.5 text-white/95
|
px-6 py-2.5 ${textClasses.primary}/95
|
||||||
hover:text-[var(--luxury-gold)] transition-all duration-300
|
hover:text-[var(--luxury-gold)] transition-all duration-300
|
||||||
font-light tracking-wider relative group overflow-hidden"
|
font-light tracking-wider relative group overflow-hidden`}
|
||||||
>
|
>
|
||||||
<span className="absolute inset-0 border border-[rgba(var(--luxury-gold-rgb),0.2)] rounded-md opacity-0 group-hover:opacity-100 transition-all duration-300 group-hover:border-[rgba(var(--luxury-gold-rgb),0.5)]"></span>
|
<span className="absolute inset-0 border border-[rgba(var(--luxury-gold-rgb),0.2)] rounded-md opacity-0 group-hover:opacity-100 transition-all duration-300 group-hover:border-[rgba(var(--luxury-gold-rgb),0.5)]"></span>
|
||||||
<span className="absolute inset-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.05)] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
<span className="absolute inset-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.05)] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
||||||
@@ -590,22 +602,26 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="font-light text-white/95 tracking-wider text-sm">
|
<span className={`font-light ${textClasses.primary}/95 tracking-wider text-sm`}>
|
||||||
{userInfo?.name}
|
{userInfo?.name}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isUserMenuOpen && (
|
{isUserMenuOpen && (
|
||||||
<div className="absolute right-0 mt-3
|
<div className={`absolute right-0 mt-3
|
||||||
w-56 bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f]
|
w-56 ${theme.theme_layout_mode === 'light'
|
||||||
rounded-lg shadow-[0_8px_32px_rgba(0,0,0,0.6)] py-3 border border-[rgba(var(--luxury-gold-rgb),0.2)]
|
? 'bg-gradient-to-b from-white via-gray-50 to-white border-gray-200'
|
||||||
z-[9999] backdrop-blur-xl animate-fade-in"
|
: 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] border-[rgba(var(--luxury-gold-rgb),0.2)]'}
|
||||||
|
rounded-lg ${theme.theme_layout_mode === 'light'
|
||||||
|
? 'shadow-[0_8px_32px_rgba(0,0,0,0.15)]'
|
||||||
|
: 'shadow-[0_8px_32px_rgba(0,0,0,0.6)]'} py-3 border
|
||||||
|
z-[9999] backdrop-blur-xl animate-fade-in`}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
to="/profile"
|
to="/profile"
|
||||||
onClick={() => setIsUserMenuOpen(false)}
|
onClick={() => setIsUserMenuOpen(false)}
|
||||||
className="flex items-center space-x-3
|
className="flex items-center space-x-3
|
||||||
px-5 py-3 text-white/95
|
px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||||
@@ -620,7 +636,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
to="/dashboard"
|
to="/dashboard"
|
||||||
onClick={() => setIsUserMenuOpen(false)}
|
onClick={() => setIsUserMenuOpen(false)}
|
||||||
className="flex items-center space-x-3
|
className="flex items-center space-x-3
|
||||||
px-5 py-3 text-white/95
|
px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||||
@@ -633,7 +649,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
to="/favorites"
|
to="/favorites"
|
||||||
onClick={() => setIsUserMenuOpen(false)}
|
onClick={() => setIsUserMenuOpen(false)}
|
||||||
className="flex items-center space-x-3
|
className="flex items-center space-x-3
|
||||||
px-5 py-3 text-white/95
|
px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||||
@@ -646,7 +662,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
to="/bookings"
|
to="/bookings"
|
||||||
onClick={() => setIsUserMenuOpen(false)}
|
onClick={() => setIsUserMenuOpen(false)}
|
||||||
className="flex items-center space-x-3
|
className="flex items-center space-x-3
|
||||||
px-5 py-3 text-white/95
|
px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||||
@@ -659,7 +675,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
to="/loyalty"
|
to="/loyalty"
|
||||||
onClick={() => setIsUserMenuOpen(false)}
|
onClick={() => setIsUserMenuOpen(false)}
|
||||||
className="flex items-center space-x-3
|
className="flex items-center space-x-3
|
||||||
px-5 py-3 text-white/95
|
px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||||
@@ -672,7 +688,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
to="/group-bookings"
|
to="/group-bookings"
|
||||||
onClick={() => setIsUserMenuOpen(false)}
|
onClick={() => setIsUserMenuOpen(false)}
|
||||||
className="flex items-center space-x-3
|
className="flex items-center space-x-3
|
||||||
px-5 py-3 text-white/95
|
px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||||
@@ -685,7 +701,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
to="/complaints"
|
to="/complaints"
|
||||||
onClick={() => setIsUserMenuOpen(false)}
|
onClick={() => setIsUserMenuOpen(false)}
|
||||||
className="flex items-center space-x-3
|
className="flex items-center space-x-3
|
||||||
px-5 py-3 text-white/95
|
px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||||
@@ -698,7 +714,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
to="/guest-requests"
|
to="/guest-requests"
|
||||||
onClick={() => setIsUserMenuOpen(false)}
|
onClick={() => setIsUserMenuOpen(false)}
|
||||||
className="flex items-center space-x-3
|
className="flex items-center space-x-3
|
||||||
px-5 py-3 text-white/95
|
px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||||
@@ -711,7 +727,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
to="/gdpr"
|
to="/gdpr"
|
||||||
onClick={() => setIsUserMenuOpen(false)}
|
onClick={() => setIsUserMenuOpen(false)}
|
||||||
className="flex items-center space-x-3
|
className="flex items-center space-x-3
|
||||||
px-5 py-3 text-white/95
|
px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
||||||
@@ -728,11 +744,11 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setIsUserMenuOpen(false)
|
setIsUserMenuOpen(false)
|
||||||
}
|
}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-3 px-5 py-3 text-white/95
|
space-x-3 px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative`}
|
||||||
>
|
>
|
||||||
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
||||||
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
||||||
@@ -745,11 +761,11 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setIsUserMenuOpen(false)
|
setIsUserMenuOpen(false)
|
||||||
}
|
}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-3 px-5 py-3 text-white/95
|
space-x-3 px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative`}
|
||||||
>
|
>
|
||||||
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
||||||
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
||||||
@@ -762,11 +778,11 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setIsUserMenuOpen(false)
|
setIsUserMenuOpen(false)
|
||||||
}
|
}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-3 px-5 py-3 text-white/95
|
space-x-3 px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative`}
|
||||||
>
|
>
|
||||||
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
||||||
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
||||||
@@ -779,11 +795,11 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setIsUserMenuOpen(false)
|
setIsUserMenuOpen(false)
|
||||||
}
|
}
|
||||||
className="flex items-center
|
className={`flex items-center
|
||||||
space-x-3 px-5 py-3 text-white/95
|
space-x-3 px-5 py-3 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.1)] hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 border-l-2 border-transparent
|
transition-all duration-300 border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] rounded-md group relative"
|
hover:border-[var(--luxury-gold)] rounded-md group relative`}
|
||||||
>
|
>
|
||||||
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
<span className="absolute left-0 top-0 bottom-0 w-0 bg-gradient-to-r from-[rgba(var(--luxury-gold-rgb),0.1)] to-transparent group-hover:w-full transition-all duration-300 rounded-md"></span>
|
||||||
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
<User className="w-4 h-4 relative z-10 transition-transform duration-300 group-hover:scale-110" />
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import React, { useRef } from 'react';
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Menu, X } from 'lucide-react';
|
import { Menu, X } from 'lucide-react';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { getThemeTextClasses } from '../utils/themeUtils';
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
isMobileMenuOpen: boolean;
|
isMobileMenuOpen: boolean;
|
||||||
@@ -27,6 +29,8 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||||||
renderMobileLinksOnly = false,
|
renderMobileLinksOnly = false,
|
||||||
mobileMenuContent
|
mobileMenuContent
|
||||||
}) => {
|
}) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const textClasses = getThemeTextClasses(theme.theme_layout_mode);
|
||||||
const mobileMenuContainerRef = useRef<HTMLDivElement>(null);
|
const mobileMenuContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const mobileMenuDropdownRef = useRef<HTMLDivElement>(null);
|
const mobileMenuDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -88,11 +92,11 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||||||
key={link.to}
|
key={link.to}
|
||||||
to={link.to}
|
to={link.to}
|
||||||
onClick={handleLinkClick}
|
onClick={handleLinkClick}
|
||||||
className="px-4 py-3 text-white/90
|
className={`px-4 py-3 ${textClasses.primary}/90
|
||||||
hover:bg-[var(--luxury-gold)]/10 hover:text-[var(--luxury-gold)]
|
hover:bg-[var(--luxury-gold)]/10 hover:text-[var(--luxury-gold)]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wide"
|
hover:border-[var(--luxury-gold)] font-light tracking-wide`}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -109,9 +113,9 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||||||
<Link
|
<Link
|
||||||
key={link.to}
|
key={link.to}
|
||||||
to={link.to}
|
to={link.to}
|
||||||
className="text-white/95 hover:text-[var(--luxury-gold)]
|
className={`${textClasses.primary}/95 hover:text-[var(--luxury-gold)]
|
||||||
transition-all duration-300 font-light px-5 py-2.5
|
transition-all duration-300 font-light px-5 py-2.5
|
||||||
relative group tracking-wider text-sm uppercase"
|
relative group tracking-wider text-sm uppercase`}
|
||||||
>
|
>
|
||||||
<span className="relative z-10 transition-all duration-300 group-hover:tracking-widest">
|
<span className="relative z-10 transition-all duration-300 group-hover:tracking-widest">
|
||||||
{link.label}
|
{link.label}
|
||||||
@@ -148,7 +152,7 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||||||
<>
|
<>
|
||||||
{/* Mobile Menu Backdrop */}
|
{/* Mobile Menu Backdrop */}
|
||||||
<div
|
<div
|
||||||
className="md:hidden fixed inset-0 bg-black/70 backdrop-blur-sm"
|
className={`md:hidden fixed inset-0 ${theme.theme_layout_mode === 'light' ? 'bg-black/40' : 'bg-black/70'} backdrop-blur-sm`}
|
||||||
style={{
|
style={{
|
||||||
top: '73px',
|
top: '73px',
|
||||||
zIndex: 99998,
|
zIndex: 99998,
|
||||||
@@ -174,12 +178,14 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||||||
{/* Mobile Menu Dropdown - Right side on mobile */}
|
{/* Mobile Menu Dropdown - Right side on mobile */}
|
||||||
<div
|
<div
|
||||||
ref={mobileMenuDropdownRef}
|
ref={mobileMenuDropdownRef}
|
||||||
className="md:hidden fixed top-[73px] right-0 w-80 max-w-[85vw]
|
className={`md:hidden fixed top-[73px] right-0 w-80 max-w-[85vw]
|
||||||
bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f]
|
${theme.theme_layout_mode === 'light'
|
||||||
|
? 'bg-gradient-to-b from-white via-gray-50 to-white'
|
||||||
|
: 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f]'}
|
||||||
shadow-[0_8px_32px_rgba(0,0,0,0.9),_-4px_0_20px_rgba(var(--luxury-gold-rgb),0.2)]
|
shadow-[0_8px_32px_rgba(0,0,0,0.9),_-4px_0_20px_rgba(var(--luxury-gold-rgb),0.2)]
|
||||||
py-4 border-l-2 border-[rgba(var(--luxury-gold-rgb),0.4)]
|
py-4 border-l-2 border-[rgba(var(--luxury-gold-rgb),0.4)]
|
||||||
backdrop-blur-xl animate-fade-in
|
backdrop-blur-xl animate-fade-in
|
||||||
overflow-y-auto h-[calc(100vh-73px)]"
|
overflow-y-auto h-[calc(100vh-73px)]`}
|
||||||
style={{
|
style={{
|
||||||
zIndex: 99999,
|
zIndex: 99999,
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
@@ -227,12 +233,12 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||||||
// Ensure mouse events work
|
// Ensure mouse events work
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="px-4 py-3.5 text-white/95
|
className={`px-4 py-3.5 ${textClasses.primary}/95
|
||||||
hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
|
hover:bg-[rgba(var(--luxury-gold-rgb),0.15)] hover:text-[var(--luxury-gold)]
|
||||||
rounded-md transition-all duration-300
|
rounded-md transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[var(--luxury-gold)] font-light tracking-wider text-sm uppercase
|
hover:border-[var(--luxury-gold)] font-light tracking-wider text-sm uppercase
|
||||||
group relative mx-2 cursor-pointer text-left w-full block"
|
group relative mx-2 cursor-pointer text-left w-full block`}
|
||||||
style={{
|
style={{
|
||||||
touchAction: 'manipulation',
|
touchAction: 'manipulation',
|
||||||
WebkitTapHighlightColor: 'transparent',
|
WebkitTapHighlightColor: 'transparent',
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ type CompanySettings = {
|
|||||||
company_address: string;
|
company_address: string;
|
||||||
chat_working_hours_start: number;
|
chat_working_hours_start: number;
|
||||||
chat_working_hours_end: number;
|
chat_working_hours_end: number;
|
||||||
|
bank_name?: string;
|
||||||
|
bank_account_number?: string;
|
||||||
|
bank_account_holder?: string;
|
||||||
|
bank_code?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CompanySettingsContextValue = {
|
type CompanySettingsContextValue = {
|
||||||
@@ -35,6 +39,10 @@ const defaultSettings: CompanySettings = {
|
|||||||
company_address: '',
|
company_address: '',
|
||||||
chat_working_hours_start: 9,
|
chat_working_hours_start: 9,
|
||||||
chat_working_hours_end: 17,
|
chat_working_hours_end: 17,
|
||||||
|
bank_name: '',
|
||||||
|
bank_account_number: '',
|
||||||
|
bank_account_holder: '',
|
||||||
|
bank_code: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const CompanySettingsContext = createContext<CompanySettingsContextValue | undefined>(undefined);
|
const CompanySettingsContext = createContext<CompanySettingsContextValue | undefined>(undefined);
|
||||||
@@ -71,6 +79,10 @@ export const CompanySettingsProvider: React.FC<CompanySettingsProviderProps> = (
|
|||||||
company_address: response.data.company_address || defaultSettings.company_address,
|
company_address: response.data.company_address || defaultSettings.company_address,
|
||||||
chat_working_hours_start: response.data.chat_working_hours_start || defaultSettings.chat_working_hours_start,
|
chat_working_hours_start: response.data.chat_working_hours_start || defaultSettings.chat_working_hours_start,
|
||||||
chat_working_hours_end: response.data.chat_working_hours_end || defaultSettings.chat_working_hours_end,
|
chat_working_hours_end: response.data.chat_working_hours_end || defaultSettings.chat_working_hours_end,
|
||||||
|
bank_name: response.data.bank_name || defaultSettings.bank_name,
|
||||||
|
bank_account_number: response.data.bank_account_number || defaultSettings.bank_account_number,
|
||||||
|
bank_account_holder: response.data.bank_account_holder || defaultSettings.bank_account_holder,
|
||||||
|
bank_code: response.data.bank_code || defaultSettings.bank_code,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,25 +7,28 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { themeService } from '../../features/system/services/systemSettingsService';
|
import { themeService } from '../../features/system/services/systemSettingsService';
|
||||||
|
|
||||||
type ThemeColors = {
|
type ThemeLayoutMode = 'dark' | 'light';
|
||||||
primary: string;
|
|
||||||
primaryLight: string;
|
type ThemeSettings = {
|
||||||
primaryDark: string;
|
theme_primary_color: string;
|
||||||
primaryAccent: string;
|
theme_primary_light: string;
|
||||||
|
theme_primary_dark: string;
|
||||||
|
theme_primary_accent: string;
|
||||||
|
theme_layout_mode: ThemeLayoutMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ThemeContextValue = {
|
type ThemeContextValue = {
|
||||||
colors: ThemeColors;
|
theme: ThemeSettings;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
refreshTheme: () => Promise<void>;
|
refreshTheme: () => Promise<void>;
|
||||||
updateColors: (colors: Partial<ThemeColors>) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultColors: ThemeColors = {
|
const defaultTheme: ThemeSettings = {
|
||||||
primary: '#d4af37',
|
theme_primary_color: '#d4af37',
|
||||||
primaryLight: '#f5d76e',
|
theme_primary_light: '#f5d76e',
|
||||||
primaryDark: '#c9a227',
|
theme_primary_dark: '#c9a227',
|
||||||
primaryAccent: '#e8c547',
|
theme_primary_accent: '#e8c547',
|
||||||
|
theme_layout_mode: 'dark',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
||||||
@@ -42,58 +45,8 @@ interface ThemeProviderProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts hex color to RGB values
|
|
||||||
*/
|
|
||||||
const hexToRgb = (hex: string): string => {
|
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
||||||
return result
|
|
||||||
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`
|
|
||||||
: '212, 175, 55'; // Default gold RGB
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies theme colors as CSS variables to the document root
|
|
||||||
*/
|
|
||||||
const applyThemeColors = (colors: ThemeColors) => {
|
|
||||||
const root = document.documentElement;
|
|
||||||
|
|
||||||
// Apply CSS variables
|
|
||||||
root.style.setProperty('--luxury-gold', colors.primary);
|
|
||||||
root.style.setProperty('--luxury-gold-light', colors.primaryLight);
|
|
||||||
root.style.setProperty('--luxury-gold-dark', colors.primaryDark);
|
|
||||||
root.style.setProperty('--luxury-gold-accent', colors.primaryAccent);
|
|
||||||
|
|
||||||
// Add RGB versions for rgba() usage
|
|
||||||
root.style.setProperty('--luxury-gold-rgb', hexToRgb(colors.primary));
|
|
||||||
root.style.setProperty('--luxury-gold-light-rgb', hexToRgb(colors.primaryLight));
|
|
||||||
root.style.setProperty('--luxury-gold-dark-rgb', hexToRgb(colors.primaryDark));
|
|
||||||
root.style.setProperty('--luxury-gold-accent-rgb', hexToRgb(colors.primaryAccent));
|
|
||||||
|
|
||||||
// Also update gradient variables
|
|
||||||
root.style.setProperty(
|
|
||||||
'--gradient-gold',
|
|
||||||
`linear-gradient(135deg, ${colors.primary} 0%, ${colors.primaryLight} 100%)`
|
|
||||||
);
|
|
||||||
root.style.setProperty(
|
|
||||||
'--gradient-gold-dark',
|
|
||||||
`linear-gradient(135deg, ${colors.primaryDark} 0%, ${colors.primary} 100%)`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update shadow variables with proper opacity
|
|
||||||
const primaryRgb = hexToRgb(colors.primary);
|
|
||||||
root.style.setProperty(
|
|
||||||
'--shadow-luxury',
|
|
||||||
`0 4px 20px rgba(${primaryRgb}, 0.15)`
|
|
||||||
);
|
|
||||||
root.style.setProperty(
|
|
||||||
'--shadow-luxury-gold',
|
|
||||||
`0 8px 30px rgba(${primaryRgb}, 0.25)`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||||
const [colors, setColors] = useState<ThemeColors>(defaultColors);
|
const [theme, setTheme] = useState<ThemeSettings>(defaultTheme);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
const loadTheme = async () => {
|
const loadTheme = async () => {
|
||||||
@@ -101,20 +54,34 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const response = await themeService.getThemeSettings();
|
const response = await themeService.getThemeSettings();
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
const newColors: ThemeColors = {
|
const themeData: ThemeSettings = {
|
||||||
primary: response.data.theme_primary_color || defaultColors.primary,
|
theme_primary_color: response.data.theme_primary_color || defaultTheme.theme_primary_color,
|
||||||
primaryLight: response.data.theme_primary_light || defaultColors.primaryLight,
|
theme_primary_light: response.data.theme_primary_light || defaultTheme.theme_primary_light,
|
||||||
primaryDark: response.data.theme_primary_dark || defaultColors.primaryDark,
|
theme_primary_dark: response.data.theme_primary_dark || defaultTheme.theme_primary_dark,
|
||||||
primaryAccent: response.data.theme_primary_accent || defaultColors.primaryAccent,
|
theme_primary_accent: response.data.theme_primary_accent || defaultTheme.theme_primary_accent,
|
||||||
|
theme_layout_mode: (response.data.theme_layout_mode === 'light' ? 'light' : 'dark') as ThemeLayoutMode,
|
||||||
};
|
};
|
||||||
setColors(newColors);
|
setTheme(themeData);
|
||||||
applyThemeColors(newColors);
|
|
||||||
|
// Apply CSS variables for colors
|
||||||
|
document.documentElement.style.setProperty('--luxury-gold', themeData.theme_primary_color);
|
||||||
|
document.documentElement.style.setProperty('--luxury-gold-light', themeData.theme_primary_light);
|
||||||
|
document.documentElement.style.setProperty('--luxury-gold-dark', themeData.theme_primary_dark);
|
||||||
|
document.documentElement.style.setProperty('--luxury-gold-accent', themeData.theme_primary_accent);
|
||||||
|
|
||||||
|
// Apply layout mode class to body
|
||||||
|
document.body.classList.remove('theme-dark', 'theme-light');
|
||||||
|
document.body.classList.add(`theme-${themeData.theme_layout_mode}`);
|
||||||
|
document.documentElement.classList.remove('theme-dark', 'theme-light');
|
||||||
|
document.documentElement.classList.add(`theme-${themeData.theme_layout_mode}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading theme settings:', error);
|
console.error('Error loading theme settings:', error);
|
||||||
// Apply default colors on error
|
// Apply defaults
|
||||||
applyThemeColors(defaultColors);
|
document.body.classList.remove('theme-dark', 'theme-light');
|
||||||
setColors(defaultColors);
|
document.body.classList.add('theme-dark');
|
||||||
|
document.documentElement.classList.remove('theme-dark', 'theme-light');
|
||||||
|
document.documentElement.classList.add('theme-dark');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -140,23 +107,15 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
|||||||
await loadTheme();
|
await loadTheme();
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateColors = (newColors: Partial<ThemeColors>) => {
|
|
||||||
const updatedColors = { ...colors, ...newColors };
|
|
||||||
setColors(updatedColors);
|
|
||||||
applyThemeColors(updatedColors);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider
|
<ThemeContext.Provider
|
||||||
value={{
|
value={{
|
||||||
colors,
|
theme,
|
||||||
isLoading,
|
isLoading,
|
||||||
refreshTheme,
|
refreshTheme,
|
||||||
updateColors,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ export const DATE_FORMATS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const CURRENCY = {
|
export const CURRENCY = {
|
||||||
VND: 'VND',
|
|
||||||
USD: 'USD',
|
USD: 'USD',
|
||||||
EUR: 'EUR',
|
EUR: 'EUR',
|
||||||
|
GBP: 'GBP',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
|
|||||||
@@ -43,9 +43,7 @@ export const formatCurrency = (
|
|||||||
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||||
|
|
||||||
if (isNaN(numAmount)) {
|
if (isNaN(numAmount)) {
|
||||||
|
return `0 ${currency || 'USD'}`;
|
||||||
if (currency === 'VND') return '0 ₫';
|
|
||||||
return `0 ${currency || 'VND'}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -67,16 +65,7 @@ export const formatCurrency = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currencyToUse = currencyToUse || 'VND';
|
currencyToUse = currencyToUse || 'USD';
|
||||||
|
|
||||||
if (currencyToUse === 'VND') {
|
|
||||||
return new Intl.NumberFormat('vi-VN', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'VND',
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
}).format(numAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const localeMap: Record<string, string> = {
|
const localeMap: Record<string, string> = {
|
||||||
@@ -242,3 +231,20 @@ export const truncateText = (
|
|||||||
return text.slice(0, maxLength - suffix.length) + suffix;
|
return text.slice(0, maxLength - suffix.length) + suffix;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format working hours from 24-hour format to 12-hour format
|
||||||
|
* @param startHour - Start hour in 24-hour format (0-23)
|
||||||
|
* @param endHour - End hour in 24-hour format (0-23)
|
||||||
|
* @returns Formatted string like "9:00 AM - 5:00 PM"
|
||||||
|
*/
|
||||||
|
export const formatWorkingHours = (startHour: number, endHour: number): string => {
|
||||||
|
const formatHour = (hour: number): string => {
|
||||||
|
if (hour === 0) return '12:00 AM';
|
||||||
|
if (hour < 12) return `${hour}:00 AM`;
|
||||||
|
if (hour === 12) return '12:00 PM';
|
||||||
|
return `${hour - 12}:00 PM`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return `${formatHour(startHour)} - ${formatHour(endHour)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
73
Frontend/src/shared/utils/themeUtils.ts
Normal file
73
Frontend/src/shared/utils/themeUtils.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Theme utility functions for applying theme-aware classes
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ThemeLayoutMode = 'dark' | 'light';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get background classes based on theme layout mode
|
||||||
|
*/
|
||||||
|
export const getThemeBackgroundClasses = (mode: ThemeLayoutMode): string => {
|
||||||
|
if (mode === 'light') {
|
||||||
|
return 'bg-gradient-to-b from-gray-50 via-white to-gray-50';
|
||||||
|
}
|
||||||
|
// Dark mode (default)
|
||||||
|
return 'bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f]';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hero section background classes based on theme layout mode
|
||||||
|
*/
|
||||||
|
export const getThemeHeroBackgroundClasses = (mode: ThemeLayoutMode): string => {
|
||||||
|
if (mode === 'light') {
|
||||||
|
return 'bg-gradient-to-br from-gray-50 via-white to-gray-50 border-b border-gray-200';
|
||||||
|
}
|
||||||
|
// Dark mode (default)
|
||||||
|
return 'bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[var(--luxury-gold)]/10';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get text color classes based on theme layout mode
|
||||||
|
*/
|
||||||
|
export const getThemeTextClasses = (mode: ThemeLayoutMode): {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
muted: string;
|
||||||
|
} => {
|
||||||
|
if (mode === 'light') {
|
||||||
|
return {
|
||||||
|
primary: 'text-gray-900',
|
||||||
|
secondary: 'text-gray-700',
|
||||||
|
muted: 'text-gray-500',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Dark mode (default)
|
||||||
|
return {
|
||||||
|
primary: 'text-white',
|
||||||
|
secondary: 'text-gray-300',
|
||||||
|
muted: 'text-gray-400',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get card/container background classes based on theme layout mode
|
||||||
|
*/
|
||||||
|
export const getThemeCardClasses = (mode: ThemeLayoutMode): string => {
|
||||||
|
if (mode === 'light') {
|
||||||
|
return 'bg-white border border-gray-200';
|
||||||
|
}
|
||||||
|
// Dark mode (default)
|
||||||
|
return 'bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] border border-[var(--luxury-gold)]/30';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get input/field background classes based on theme layout mode
|
||||||
|
*/
|
||||||
|
export const getThemeInputClasses = (mode: ThemeLayoutMode): string => {
|
||||||
|
if (mode === 'light') {
|
||||||
|
return 'bg-white border-gray-300 text-gray-900';
|
||||||
|
}
|
||||||
|
// Dark mode (default)
|
||||||
|
return 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-gray-700 text-white';
|
||||||
|
};
|
||||||
|
|
||||||
149
ICON_CONTROL_AUDIT.md
Normal file
149
ICON_CONTROL_AUDIT.md
Normal 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
221
SETTINGS_USAGE_AUDIT.md
Normal 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
|
||||||
|
|
||||||
Reference in New Issue
Block a user