update
This commit is contained in:
@@ -1,636 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Hotel,
|
||||
Heart,
|
||||
MapPin,
|
||||
Phone,
|
||||
Mail,
|
||||
Linkedin,
|
||||
Twitter
|
||||
} from 'lucide-react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import { createSanitizedHtml } from '../utils/htmlSanitizer';
|
||||
|
||||
const AboutPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
const response = await pageContentService.getAboutContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
setPageContent(response.data.page_content);
|
||||
|
||||
|
||||
if (response.data.page_content.meta_title) {
|
||||
document.title = response.data.page_content.meta_title;
|
||||
}
|
||||
if (response.data.page_content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', response.data.page_content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
|
||||
const displayPhone = settings.company_phone || '+1 (234) 567-890';
|
||||
const displayEmail = settings.company_email || 'info@luxuryhotel.com';
|
||||
const displayAddress = settings.company_address || '123 Luxury Street\nCity, State 12345\nCountry';
|
||||
|
||||
|
||||
const defaultValues = [
|
||||
{
|
||||
icon: 'Heart',
|
||||
title: 'Passion',
|
||||
description: 'We are passionate about hospitality and dedicated to creating exceptional experiences for every guest.'
|
||||
},
|
||||
{
|
||||
icon: 'Award',
|
||||
title: 'Excellence',
|
||||
description: 'We strive for excellence in every aspect of our service, from the smallest detail to the grandest gesture.'
|
||||
},
|
||||
{
|
||||
icon: 'Shield',
|
||||
title: 'Integrity',
|
||||
description: 'We conduct our business with honesty, transparency, and respect for our guests and community.'
|
||||
},
|
||||
{
|
||||
icon: 'Users',
|
||||
title: 'Service',
|
||||
description: 'Our guests are at the heart of everything we do. Your comfort and satisfaction are our top priorities.'
|
||||
}
|
||||
];
|
||||
|
||||
const defaultFeatures = [
|
||||
{
|
||||
icon: 'Star',
|
||||
title: 'Premium Accommodations',
|
||||
description: 'Luxuriously appointed rooms and suites designed for ultimate comfort and relaxation.'
|
||||
},
|
||||
{
|
||||
icon: 'Clock',
|
||||
title: '24/7 Service',
|
||||
description: 'Round-the-clock concierge and room service to attend to your needs at any time.'
|
||||
},
|
||||
{
|
||||
icon: 'Award',
|
||||
title: 'Award-Winning',
|
||||
description: 'Recognized for excellence in hospitality and guest satisfaction.'
|
||||
}
|
||||
];
|
||||
|
||||
const values = pageContent?.values && pageContent.values.length > 0
|
||||
? pageContent.values.map((v: any) => ({
|
||||
icon: v.icon || defaultValues.find(d => d.title === v.title)?.icon || 'Heart',
|
||||
title: v.title,
|
||||
description: v.description
|
||||
}))
|
||||
: defaultValues;
|
||||
|
||||
const features = pageContent?.features && pageContent.features.length > 0
|
||||
? pageContent.features.map((f: any) => ({
|
||||
icon: f.icon || defaultFeatures.find(d => d.title === f.title)?.icon || 'Star',
|
||||
title: f.title,
|
||||
description: f.description
|
||||
}))
|
||||
: defaultFeatures;
|
||||
|
||||
|
||||
const team = pageContent?.team && typeof pageContent.team === 'string'
|
||||
? JSON.parse(pageContent.team)
|
||||
: (Array.isArray(pageContent?.team) ? pageContent.team : []);
|
||||
const timeline = pageContent?.timeline && typeof pageContent.timeline === 'string'
|
||||
? JSON.parse(pageContent.timeline)
|
||||
: (Array.isArray(pageContent?.timeline) ? pageContent.timeline : []);
|
||||
const achievements = pageContent?.achievements && typeof pageContent.achievements === 'string'
|
||||
? JSON.parse(pageContent.achievements)
|
||||
: (Array.isArray(pageContent?.achievements) ? pageContent.achievements : []);
|
||||
|
||||
|
||||
const getIconComponent = (iconName?: string) => {
|
||||
if (!iconName) return Heart;
|
||||
const IconComponent = (LucideIcons as any)[iconName] || Heart;
|
||||
return IconComponent;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-50 via-white to-slate-50">
|
||||
{}
|
||||
<div className={`relative ${pageContent?.about_hero_image ? '' : 'bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900'} text-white py-20 md:py-24 ${pageContent?.about_hero_image ? 'h-[400px] md:h-[450px] lg:h-[500px]' : ''} overflow-hidden`}>
|
||||
{pageContent?.about_hero_image && (
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={pageContent.about_hero_image.startsWith('http') ? pageContent.about_hero_image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}/${pageContent.about_hero_image}`}
|
||||
alt="About Hero"
|
||||
className="w-full h-full object-cover scale-105 transition-transform duration-[20s] ease-out hover:scale-100"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/50 to-black/70"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#d4af37]/10 via-transparent to-[#d4af37]/10"></div>
|
||||
</div>
|
||||
)}
|
||||
{!pageContent?.about_hero_image && (
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(212,175,55,0.1),transparent_70%)]"></div>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[linear-gradient(45deg,transparent_30%,rgba(212,175,55,0.05)_50%,transparent_70%)]"></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
{!pageContent?.about_hero_image && (
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-full blur-3xl opacity-40 animate-pulse"></div>
|
||||
<div className="relative bg-gradient-to-br from-[#d4af37] to-[#c9a227] p-6 rounded-2xl shadow-2xl shadow-[#d4af37]/30">
|
||||
<Hotel className="w-12 h-12 md:w-16 md:h-16 text-white drop-shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-6">
|
||||
<div className="inline-block mb-4">
|
||||
<div className="h-px w-20 bg-gradient-to-r from-transparent via-[#d4af37] to-transparent mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-serif font-light mb-6 tracking-[0.02em] leading-tight">
|
||||
<span className="bg-gradient-to-b from-white via-[#f5d76e] to-[#d4af37] bg-clip-text text-transparent drop-shadow-2xl">
|
||||
{pageContent?.title || 'About Luxury Hotel'}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl lg:text-2xl text-gray-200 font-light leading-relaxed max-w-2xl mx-auto tracking-wide">
|
||||
{pageContent?.subtitle || pageContent?.description || 'Where Excellence Meets Unforgettable Experiences'}
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<div className="inline-block">
|
||||
<div className="h-px w-20 bg-gradient-to-r from-transparent via-[#d4af37] to-transparent mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<section className="py-20 md:py-28 bg-white relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[#d4af37]/30 to-transparent"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Our Heritage</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Our Story
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="prose prose-lg md:prose-xl max-w-none text-gray-700 leading-relaxed space-y-8">
|
||||
{pageContent?.story_content ? (
|
||||
<div
|
||||
className="text-lg md:text-xl leading-relaxed font-light tracking-wide"
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(pageContent.story_content.replace(/\n/g, '<br />'))}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-lg md:text-xl leading-relaxed font-light tracking-wide first-letter:text-5xl first-letter:font-serif first-letter:text-[#d4af37] first-letter:float-left first-letter:mr-2 first-letter:leading-none">
|
||||
Welcome to Luxury Hotel, where timeless elegance meets modern sophistication.
|
||||
Since our founding, we have been dedicated to providing exceptional hospitality
|
||||
and creating unforgettable memories for our guests.
|
||||
</p>
|
||||
<p className="text-lg md:text-xl leading-relaxed font-light tracking-wide">
|
||||
Nestled in the heart of the city, our hotel combines classic architecture with
|
||||
contemporary amenities, offering a perfect blend of comfort and luxury. Every
|
||||
detail has been carefully curated to ensure your stay exceeds expectations.
|
||||
</p>
|
||||
<p className="text-lg md:text-xl leading-relaxed font-light tracking-wide">
|
||||
Our commitment to excellence extends beyond our beautiful rooms and facilities.
|
||||
We believe in creating meaningful connections with our guests, understanding
|
||||
their needs, and delivering personalized service that makes each visit special.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[#d4af37]/30 to-transparent"></div>
|
||||
</section>
|
||||
|
||||
{}
|
||||
<section className="py-20 md:py-28 bg-gradient-to-b from-slate-50 via-white to-slate-50 relative">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_50%,rgba(212,175,55,0.03),transparent_50%)]"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Core Principles</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Our Values
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||
{values.map((value, index) => (
|
||||
<div
|
||||
key={value.title}
|
||||
className="group relative bg-white/80 backdrop-blur-sm p-8 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[#d4af37]/30 hover:-translate-y-2"
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-[#d4af37]/5 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-xl flex items-center justify-center mb-6 shadow-lg shadow-[#d4af37]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
{(() => {
|
||||
const ValueIcon = getIconComponent(value.icon);
|
||||
return <ValueIcon className="w-8 h-8 text-white drop-shadow-md" />;
|
||||
})()}
|
||||
</div>
|
||||
<h3 className="text-xl md:text-2xl font-serif font-semibold text-gray-900 mb-3 group-hover:text-[#d4af37] transition-colors duration-300">
|
||||
{value.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 leading-relaxed font-light text-sm md:text-base">
|
||||
{value.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{}
|
||||
<section className="py-20 md:py-28 bg-white relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(135deg,transparent_0%,rgba(212,175,55,0.02)_50%,transparent_100%)]"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Excellence Defined</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Why Choose Us
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12">
|
||||
{features.map((feature, index) => {
|
||||
const FeatureIcon = getIconComponent(feature.icon);
|
||||
return (
|
||||
<div
|
||||
key={feature.title || index}
|
||||
className="group text-center p-8 relative"
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37]/5 to-transparent rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-xl shadow-[#d4af37]/30 group-hover:scale-110 group-hover:shadow-2xl group-hover:shadow-[#d4af37]/40 transition-all duration-500">
|
||||
<FeatureIcon className="w-10 h-10 text-white drop-shadow-lg" />
|
||||
</div>
|
||||
<h3 className="text-xl md:text-2xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[#d4af37] transition-colors duration-300">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 leading-relaxed font-light text-sm md:text-base">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{}
|
||||
{(pageContent?.mission || pageContent?.vision) && (
|
||||
<section className="py-20 md:py-28 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 relative overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(212,175,55,0.1),transparent_70%)]"></div>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[linear-gradient(45deg,transparent_30%,rgba(212,175,55,0.05)_50%,transparent_70%)]"></div>
|
||||
</div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8 lg:gap-12">
|
||||
{pageContent.mission && (
|
||||
<div className="group relative bg-white/95 backdrop-blur-sm p-10 md:p-12 rounded-2xl shadow-2xl border border-[#d4af37]/20 hover:border-[#d4af37]/40 transition-all duration-500 hover:shadow-[#d4af37]/20 hover:-translate-y-1">
|
||||
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-[#d4af37]/10 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-1 h-12 bg-gradient-to-b from-[#d4af37] to-[#f5d76e] rounded-full"></div>
|
||||
<h2 className="text-3xl md:text-4xl font-serif font-light text-gray-900">Our Mission</h2>
|
||||
</div>
|
||||
<p className="text-gray-700 leading-relaxed text-base md:text-lg font-light tracking-wide">{pageContent.mission}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{pageContent.vision && (
|
||||
<div className="group relative bg-white/95 backdrop-blur-sm p-10 md:p-12 rounded-2xl shadow-2xl border border-[#d4af37]/20 hover:border-[#d4af37]/40 transition-all duration-500 hover:shadow-[#d4af37]/20 hover:-translate-y-1">
|
||||
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-[#d4af37]/10 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-1 h-12 bg-gradient-to-b from-[#d4af37] to-[#f5d76e] rounded-full"></div>
|
||||
<h2 className="text-3xl md:text-4xl font-serif font-light text-gray-900">Our Vision</h2>
|
||||
</div>
|
||||
<p className="text-gray-700 leading-relaxed text-base md:text-lg font-light tracking-wide">{pageContent.vision}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{}
|
||||
{team && team.length > 0 && (
|
||||
<section className="py-20 md:py-28 bg-gradient-to-b from-white via-slate-50 to-white relative">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Meet The Experts</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Our Team
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-10">
|
||||
{team.map((member: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="group relative bg-white rounded-2xl shadow-xl overflow-hidden hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[#d4af37]/30 hover:-translate-y-2"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37]/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-10"></div>
|
||||
{member.image && (
|
||||
<div className="relative overflow-hidden h-72">
|
||||
<img
|
||||
src={member.image.startsWith('http') ? member.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}/${member.image}`}
|
||||
alt={member.name}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-8 relative z-10">
|
||||
<h3 className="text-2xl font-serif font-semibold text-gray-900 mb-2 group-hover:text-[#d4af37] transition-colors duration-300">{member.name}</h3>
|
||||
<p className="text-[#d4af37] font-medium mb-4 text-sm tracking-wide uppercase">{member.role}</p>
|
||||
{member.bio && <p className="text-gray-600 text-sm mb-6 leading-relaxed font-light">{member.bio}</p>}
|
||||
{member.social_links && (
|
||||
<div className="flex gap-4 pt-4 border-t border-gray-100">
|
||||
{member.social_links.linkedin && (
|
||||
<a
|
||||
href={member.social_links.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 hover:bg-[#d4af37] hover:text-white transition-all duration-300 group-hover:scale-110"
|
||||
>
|
||||
<Linkedin className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
{member.social_links.twitter && (
|
||||
<a
|
||||
href={member.social_links.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 hover:bg-[#d4af37] hover:text-white transition-all duration-300 group-hover:scale-110"
|
||||
>
|
||||
<Twitter className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{}
|
||||
{timeline && timeline.length > 0 && (
|
||||
<section className="py-20 md:py-28 bg-gradient-to-b from-slate-50 via-white to-slate-50 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_70%_50%,rgba(212,175,55,0.03),transparent_50%)]"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Our Journey</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Our History
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute left-8 md:left-1/2 transform md:-translate-x-1/2 w-1 h-full bg-gradient-to-b from-[#d4af37] via-[#f5d76e] to-[#d4af37] shadow-lg"></div>
|
||||
<div className="space-y-12 md:space-y-16">
|
||||
{timeline.map((event: any, index: number) => (
|
||||
<div key={index} className={`relative flex items-center ${index % 2 === 0 ? 'md:flex-row' : 'md:flex-row-reverse'}`}>
|
||||
<div className="absolute left-6 md:left-1/2 transform md:-translate-x-1/2 w-6 h-6 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full border-4 border-white shadow-xl z-10 group-hover:scale-125 transition-transform duration-300"></div>
|
||||
<div className={`ml-20 md:ml-0 md:w-5/12 ${index % 2 === 0 ? 'md:mr-auto md:pr-8' : 'md:ml-auto md:pl-8'}`}>
|
||||
<div className="group bg-white/90 backdrop-blur-sm p-8 rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[#d4af37]/30">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="text-[#d4af37] font-bold text-2xl md:text-3xl font-serif">{event.year}</div>
|
||||
<div className="h-px flex-1 bg-gradient-to-r from-[#d4af37] to-transparent"></div>
|
||||
</div>
|
||||
<h3 className="text-2xl md:text-3xl font-serif font-semibold text-gray-900 mb-3 group-hover:text-[#d4af37] transition-colors duration-300">{event.title}</h3>
|
||||
<p className="text-gray-600 leading-relaxed font-light mb-4">{event.description}</p>
|
||||
{event.image && (
|
||||
<div className="mt-6 overflow-hidden rounded-xl">
|
||||
<img
|
||||
src={event.image.startsWith('http') ? event.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}/${event.image}`}
|
||||
alt={event.title}
|
||||
className="w-full h-56 md:h-64 object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{}
|
||||
{achievements && achievements.length > 0 && (
|
||||
<section className="py-20 md:py-28 bg-white relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(135deg,transparent_0%,rgba(212,175,55,0.02)_50%,transparent_100%)]"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Recognition</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Achievements & Awards
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-10">
|
||||
{achievements.map((achievement: any, index: number) => {
|
||||
const AchievementIcon = getIconComponent(achievement.icon);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="group relative bg-gradient-to-br from-white to-slate-50 p-8 rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 border border-gray-100 hover:border-[#d4af37]/40 hover:-translate-y-2"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-[#d4af37]/10 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-xl flex items-center justify-center shadow-lg shadow-[#d4af37]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<AchievementIcon className="w-8 h-8 text-white drop-shadow-md" />
|
||||
</div>
|
||||
{achievement.year && (
|
||||
<div className="text-[#d4af37] font-bold text-2xl font-serif">{achievement.year}</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-xl md:text-2xl font-serif font-semibold text-gray-900 mb-3 group-hover:text-[#d4af37] transition-colors duration-300">{achievement.title}</h3>
|
||||
<p className="text-gray-600 text-sm md:text-base leading-relaxed font-light mb-4">{achievement.description}</p>
|
||||
{achievement.image && (
|
||||
<div className="mt-6 overflow-hidden rounded-xl">
|
||||
<img
|
||||
src={achievement.image.startsWith('http') ? achievement.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}/${achievement.image}`}
|
||||
alt={achievement.title}
|
||||
className="w-full h-40 object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{}
|
||||
<section className="py-20 md:py-28 bg-gradient-to-br from-slate-50 via-white to-slate-50 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(212,175,55,0.03),transparent_70%)]"></div>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-block mb-4">
|
||||
<span className="text-sm font-semibold text-[#d4af37] tracking-[0.2em] uppercase">Connect With Us</span>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-light text-gray-900 mb-6 tracking-tight">
|
||||
Get In Touch
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-[#d4af37]"></div>
|
||||
<div className="w-2 h-2 bg-[#d4af37] rounded-full"></div>
|
||||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-[#d4af37]"></div>
|
||||
</div>
|
||||
<p className="text-gray-600 mt-6 text-lg font-light max-w-2xl mx-auto">
|
||||
We'd love to hear from you. Contact us for reservations or inquiries.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-10 mb-16">
|
||||
<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-[#d4af37]/40 hover:-translate-y-2">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[#d4af37]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<MapPin className="w-8 h-8 text-white drop-shadow-md" />
|
||||
</div>
|
||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[#d4af37] transition-colors duration-300">
|
||||
Address
|
||||
</h3>
|
||||
<p className="text-gray-600 leading-relaxed font-light">
|
||||
{displayAddress
|
||||
.split('\n').map((line, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{line}
|
||||
{i < displayAddress.split('\n').length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
<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-[#d4af37]/40 hover:-translate-y-2" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[#d4af37]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<Phone className="w-8 h-8 text-white drop-shadow-md" />
|
||||
</div>
|
||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[#d4af37] transition-colors duration-300">
|
||||
Phone
|
||||
</h3>
|
||||
<p className="text-gray-600 font-light">
|
||||
<a href={`tel:${displayPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`} className="hover:text-[#d4af37] transition-colors duration-300">
|
||||
{displayPhone}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<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-[#d4af37]/40 hover:-translate-y-2" style={{ animationDelay: '0.2s' }}>
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#d4af37] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[#d4af37]/20 group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500">
|
||||
<Mail className="w-8 h-8 text-white drop-shadow-md" />
|
||||
</div>
|
||||
<h3 className="text-xl font-serif font-semibold text-gray-900 mb-4 group-hover:text-[#d4af37] transition-colors duration-300">
|
||||
Email
|
||||
</h3>
|
||||
<p className="text-gray-600 font-light">
|
||||
<a href={`mailto:${displayEmail}`} className="hover:text-[#d4af37] transition-colors duration-300">
|
||||
{displayEmail}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<Link
|
||||
to="/rooms"
|
||||
className="group inline-flex items-center space-x-3 px-10 py-4 bg-gradient-to-r from-[#d4af37] via-[#f5d76e] to-[#d4af37] text-white rounded-xl hover:shadow-2xl hover:shadow-[#d4af37]/40 transition-all duration-500 font-medium text-lg tracking-wide relative overflow-hidden"
|
||||
>
|
||||
<span className="relative z-10">Explore Our Rooms</span>
|
||||
<Hotel className="w-5 h-5 relative z-10 group-hover:translate-x-1 transition-transform duration-300" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#f5d76e] to-[#d4af37] opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutPage;
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Accessibility, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
import { createSanitizedHtml } from '../utils/htmlSanitizer';
|
||||
|
||||
const AccessibilityPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getAccessibilityContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
if (content.content) {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Accessibility className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Accessibility</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<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-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<Accessibility className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</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-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Accessibility'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
For accessibility inquiries, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessibilityPage;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { SidebarAccountant } from '../components/layout';
|
||||
import { useResponsive } from '../hooks';
|
||||
import SidebarAccountant from '../shared/components/SidebarAccountant';
|
||||
import { useResponsive } from '../shared/hooks/useResponsive';
|
||||
|
||||
const AccountantLayout: React.FC = () => {
|
||||
const { isMobile } = useResponsive();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState, Suspense, useEffect } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
import { SidebarAdmin } from '../components/layout';
|
||||
import { SidebarAdmin } from '../shared/components';
|
||||
import { Sparkles, Zap } from 'lucide-react';
|
||||
import { useResponsive } from '../hooks';
|
||||
import AIAssistantWidget from '../features/ai/components/AIAssistantWidget';
|
||||
|
||||
// Luxury Loading Overlay
|
||||
const LuxuryLoadingOverlay: React.FC = () => {
|
||||
@@ -106,6 +107,9 @@ const AdminLayout: React.FC = () => {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* AI Assistant Widget */}
|
||||
<AIAssistantWidget />
|
||||
|
||||
{/* Custom CSS for shimmer animation */}
|
||||
<style>{`
|
||||
@keyframes shimmer {
|
||||
|
||||
@@ -1,416 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { Calendar, User, Tag, ArrowLeft, Eye, Share2, ArrowRight } from 'lucide-react';
|
||||
import { blogService, BlogPost } from '../services/api';
|
||||
import Loading from '../components/common/Loading';
|
||||
import { createSanitizedHtml } from '../utils/htmlSanitizer';
|
||||
|
||||
const BlogDetailPage: React.FC = () => {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [post, setPost] = useState<BlogPost | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [relatedPosts, setRelatedPosts] = useState<BlogPost[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (slug) {
|
||||
fetchPost();
|
||||
}
|
||||
}, [slug]);
|
||||
|
||||
const fetchPost = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await blogService.getBlogPostBySlug(slug!);
|
||||
|
||||
if (response.status === 'success' && response.data) {
|
||||
const postData = response.data.post;
|
||||
setPost(postData);
|
||||
|
||||
// Set meta tags
|
||||
if (postData.meta_title) {
|
||||
document.title = postData.meta_title;
|
||||
}
|
||||
if (postData.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', postData.meta_description);
|
||||
}
|
||||
|
||||
// Fetch related posts
|
||||
if (postData.tags && postData.tags.length > 0) {
|
||||
fetchRelatedPosts(postData.tags[0]);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching blog post:', error);
|
||||
if (error.response?.status === 404) {
|
||||
navigate('/blog');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRelatedPosts = async (tag: string) => {
|
||||
try {
|
||||
const response = await blogService.getBlogPosts({
|
||||
page: 1,
|
||||
limit: 3,
|
||||
tag,
|
||||
published_only: true,
|
||||
});
|
||||
|
||||
if (response.status === 'success' && response.data) {
|
||||
// Filter out current post
|
||||
const related = response.data.posts.filter((p: BlogPost) => p.slug !== slug);
|
||||
setRelatedPosts(related.slice(0, 3));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching related posts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return '';
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
if (navigator.share && post) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: post.title,
|
||||
text: post.excerpt,
|
||||
url: window.location.href,
|
||||
});
|
||||
} catch (error) {
|
||||
// User cancelled or error occurred
|
||||
}
|
||||
} else {
|
||||
// Fallback: copy to clipboard
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
alert('Link copied to clipboard!');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black text-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Post not found</h1>
|
||||
<Link to="/blog" className="text-[#d4af37] hover:underline">
|
||||
Back to Blog
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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' }}>
|
||||
{/* Hero Section with Featured Image */}
|
||||
{post.featured_image && (
|
||||
<div className="relative w-full h-64 sm:h-80 lg:h-96 overflow-hidden">
|
||||
<img
|
||||
src={post.featured_image}
|
||||
alt={post.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="w-full py-8 sm:py-12 lg:py-16">
|
||||
<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">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Back Button */}
|
||||
<Link
|
||||
to="/blog"
|
||||
className="inline-flex items-center gap-2 text-gray-400 hover:text-[#d4af37] transition-colors mb-8"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Blog</span>
|
||||
</Link>
|
||||
|
||||
{/* Article Header */}
|
||||
<div className="mb-8">
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{post.tags.map((tag) => (
|
||||
<Link
|
||||
key={tag}
|
||||
to={`/blog?tag=${encodeURIComponent(tag)}`}
|
||||
className="inline-flex items-center gap-1 px-3 py-1 bg-[#d4af37]/10 text-[#d4af37] rounded-full text-sm font-medium hover:bg-[#d4af37]/20 transition-colors"
|
||||
>
|
||||
<Tag className="w-3 h-3" />
|
||||
{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-display font-bold text-white mb-6 leading-tight">
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
{post.excerpt && (
|
||||
<p className="text-xl text-gray-300 mb-6 font-light leading-relaxed">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-6 text-sm text-gray-400 pb-6 border-b border-[#d4af37]/20">
|
||||
{post.published_at && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>{formatDate(post.published_at)}</span>
|
||||
</div>
|
||||
)}
|
||||
{post.author_name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
<span>{post.author_name}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye className="w-4 h-4" />
|
||||
<span>{post.views_count} views</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="flex items-center gap-2 text-[#d4af37] hover:text-[#f5d76e] transition-colors"
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
<span>Share</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Article Content */}
|
||||
<article className="prose prose-invert prose-lg max-w-none mb-12
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-6 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-3
|
||||
prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-6
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-6
|
||||
prose-ol:text-gray-300 prose-ol:font-light prose-ol:my-6
|
||||
prose-li:text-gray-300 prose-li:mb-2
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
prose-img:rounded-xl prose-img:shadow-2xl
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
>
|
||||
<div
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
post.content || '<p>No content available.</p>'
|
||||
)}
|
||||
/>
|
||||
</article>
|
||||
|
||||
{/* Luxury Sections */}
|
||||
{post.sections && post.sections.length > 0 && (
|
||||
<div className="space-y-16 mb-12">
|
||||
{post.sections
|
||||
.filter((section) => section.is_visible !== false) // Only show visible sections
|
||||
.map((section, index) => (
|
||||
<div key={index}>
|
||||
{/* Hero Section */}
|
||||
{section.type === 'hero' && (
|
||||
<div className="relative rounded-3xl overflow-hidden border-2 border-[#d4af37]/20 shadow-2xl">
|
||||
{section.image && (
|
||||
<div className="absolute inset-0">
|
||||
<img src={section.image} alt={section.title} className="w-full h-full object-cover" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/50 to-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative px-8 py-16 md:px-16 md:py-24 text-center">
|
||||
{section.title && (
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-serif font-bold text-white mb-6">
|
||||
{section.title}
|
||||
</h2>
|
||||
)}
|
||||
{section.content && (
|
||||
<p className="text-xl md:text-2xl text-gray-200 font-light leading-relaxed max-w-3xl mx-auto">
|
||||
{section.content}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Text Section */}
|
||||
{section.type === 'text' && (
|
||||
<div className={`bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] rounded-3xl border-2 border-[#d4af37]/20 p-8 md:p-12 shadow-xl ${
|
||||
section.alignment === 'center' ? 'text-center' : section.alignment === 'right' ? 'text-right' : 'text-left'
|
||||
}`}>
|
||||
{section.title && (
|
||||
<h3 className="text-3xl md:text-4xl font-serif font-bold text-white mb-6">
|
||||
{section.title}
|
||||
</h3>
|
||||
)}
|
||||
{section.content && (
|
||||
<div
|
||||
className="text-gray-300 font-light leading-relaxed text-lg"
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(section.content)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image Section */}
|
||||
{section.type === 'image' && section.image && (
|
||||
<div className="rounded-3xl overflow-hidden border-2 border-[#d4af37]/20 shadow-2xl">
|
||||
<img src={section.image} alt={section.title || 'Blog image'} className="w-full h-auto" />
|
||||
{section.title && (
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] px-6 py-4 border-t border-[#d4af37]/20">
|
||||
<p className="text-gray-400 text-sm font-light italic text-center">{section.title}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gallery Section */}
|
||||
{section.type === 'gallery' && section.images && section.images.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{section.images.map((img, imgIndex) => (
|
||||
<div key={imgIndex} className="rounded-2xl overflow-hidden border-2 border-[#d4af37]/20 shadow-xl group hover:border-[#d4af37]/50 transition-all">
|
||||
<img src={img} alt={`Gallery image ${imgIndex + 1}`} className="w-full h-64 object-cover group-hover:scale-110 transition-transform duration-500" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quote Section */}
|
||||
{section.type === 'quote' && (
|
||||
<div className="relative bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] rounded-3xl border-2 border-[#d4af37]/30 p-8 md:p-12 shadow-2xl">
|
||||
<div className="absolute top-6 left-6 text-6xl text-[#d4af37]/20 font-serif">"</div>
|
||||
{section.quote && (
|
||||
<blockquote className="text-2xl md:text-3xl font-serif font-light text-white italic mb-6 relative z-10 pl-8">
|
||||
{section.quote}
|
||||
</blockquote>
|
||||
)}
|
||||
{section.author && (
|
||||
<cite className="text-[#d4af37] text-lg font-medium not-italic block text-right">
|
||||
— {section.author}
|
||||
</cite>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Features Section */}
|
||||
{section.type === 'features' && section.features && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{section.features.map((feature, featIndex) => (
|
||||
<div key={featIndex} className="bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] rounded-2xl border-2 border-[#d4af37]/20 p-6 shadow-xl hover:border-[#d4af37]/50 transition-all">
|
||||
{feature.icon && (
|
||||
<div className="text-4xl mb-4">{feature.icon}</div>
|
||||
)}
|
||||
<h4 className="text-xl font-bold text-white mb-3">{feature.title}</h4>
|
||||
<p className="text-gray-300 font-light leading-relaxed">{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CTA Section */}
|
||||
{section.type === 'cta' && (
|
||||
<div className="relative bg-gradient-to-br from-[#d4af37]/10 via-[#c9a227]/5 to-[#d4af37]/10 rounded-3xl border-2 border-[#d4af37]/30 p-8 md:p-12 text-center shadow-2xl">
|
||||
{section.title && (
|
||||
<h3 className="text-3xl md:text-4xl font-serif font-bold text-white mb-4">
|
||||
{section.title}
|
||||
</h3>
|
||||
)}
|
||||
{section.content && (
|
||||
<p className="text-xl text-gray-300 font-light mb-8 max-w-2xl mx-auto">
|
||||
{section.content}
|
||||
</p>
|
||||
)}
|
||||
{section.cta_text && section.cta_link && (
|
||||
<a
|
||||
href={section.cta_link}
|
||||
className="inline-flex items-center gap-3 px-8 py-4 bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] font-semibold rounded-xl hover:from-[#f5d76e] hover:to-[#d4af37] transition-all shadow-lg hover:shadow-xl hover:scale-105"
|
||||
>
|
||||
{section.cta_text}
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Video Section */}
|
||||
{section.type === 'video' && section.video_url && (
|
||||
<div className="rounded-3xl overflow-hidden border-2 border-[#d4af37]/20 shadow-2xl bg-black">
|
||||
<div className="aspect-video">
|
||||
<iframe
|
||||
src={section.video_url.replace('watch?v=', 'embed/').replace('vimeo.com/', 'player.vimeo.com/video/')}
|
||||
className="w-full h-full"
|
||||
allowFullScreen
|
||||
title="Blog video"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Related Posts */}
|
||||
{relatedPosts.length > 0 && (
|
||||
<div className="mt-16 pt-12 border-t border-[#d4af37]/20">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-white mb-8">Related Posts</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{relatedPosts.map((relatedPost) => (
|
||||
<Link
|
||||
key={relatedPost.id}
|
||||
to={`/blog/${relatedPost.slug}`}
|
||||
className="group bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] rounded-xl border border-[#d4af37]/20 overflow-hidden hover:border-[#d4af37]/50 transition-all duration-300"
|
||||
>
|
||||
{relatedPost.featured_image && (
|
||||
<div className="relative h-40 overflow-hidden">
|
||||
<img
|
||||
src={relatedPost.featured_image}
|
||||
alt={relatedPost.title}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-bold text-white mb-2 group-hover:text-[#d4af37] transition-colors line-clamp-2">
|
||||
{relatedPost.title}
|
||||
</h3>
|
||||
{relatedPost.excerpt && (
|
||||
<p className="text-gray-400 text-sm line-clamp-2 font-light">
|
||||
{relatedPost.excerpt}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogDetailPage;
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Calendar, User, Tag, Search, ArrowRight, Eye, Sparkles, BookOpen } from 'lucide-react';
|
||||
import { blogService, BlogPost } from '../services/api';
|
||||
import Loading from '../components/common/Loading';
|
||||
import Pagination from '../components/common/Pagination';
|
||||
|
||||
const BlogPage: React.FC = () => {
|
||||
const [posts, setPosts] = useState<BlogPost[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedTag, setSelectedTag] = useState<string | null>(null);
|
||||
const [allTags, setAllTags] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPosts();
|
||||
}, [currentPage, searchTerm, selectedTag]);
|
||||
|
||||
const fetchPosts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await blogService.getBlogPosts({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
search: searchTerm || undefined,
|
||||
tag: selectedTag || undefined,
|
||||
published_only: true,
|
||||
});
|
||||
|
||||
if (response.status === 'success' && response.data) {
|
||||
setPosts(response.data.posts);
|
||||
setTotalPages(response.data.pagination.pages);
|
||||
setTotal(response.data.pagination.total);
|
||||
|
||||
// Use all_tags from API response if available, otherwise extract from current page
|
||||
if (response.data.all_tags && Array.isArray(response.data.all_tags)) {
|
||||
setAllTags(response.data.all_tags);
|
||||
} else {
|
||||
// Fallback: extract tags from current page posts
|
||||
const tags = new Set<string>();
|
||||
response.data.posts.forEach((post: BlogPost) => {
|
||||
post.tags?.forEach(tag => tags.add(tag));
|
||||
});
|
||||
setAllTags(Array.from(tags));
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching blog posts:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return '';
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
if (loading && posts.length === 0) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
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' }}>
|
||||
{/* Hero Section */}
|
||||
<div className="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[#d4af37]/10 pt-6 sm:pt-7 md:pt-8 overflow-hidden relative">
|
||||
{/* Background Effects */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[#d4af37] rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-10 right-10 w-40 sm:w-64 h-40 sm:h-64 bg-[#c9a227] rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[#d4af37]/50 to-transparent"></div>
|
||||
|
||||
<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 sm:py-5 md:py-6 relative z-10">
|
||||
<div className="max-w-2xl mx-auto text-center px-2">
|
||||
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#c9a227] 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-[#d4af37]/40 backdrop-blur-sm shadow-xl shadow-[#d4af37]/20 group-hover:border-[#d4af37]/60 transition-all duration-300">
|
||||
<BookOpen className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[#d4af37] drop-shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
|
||||
<span className="bg-gradient-to-r from-white via-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
Our Blog
|
||||
</span>
|
||||
</h1>
|
||||
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[#d4af37] 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">
|
||||
Discover stories, insights, and updates from our luxury hotel
|
||||
</p>
|
||||
<div className="mt-4 flex items-center justify-center gap-2 text-[#d4af37]/60">
|
||||
<Sparkles className="w-4 h-4 animate-pulse" />
|
||||
<span className="text-xs sm:text-sm font-light tracking-wider uppercase">Premium Content</span>
|
||||
<Sparkles className="w-4 h-4 animate-pulse" style={{ animationDelay: '0.5s' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[#d4af37]/30 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
{/* Main Content - Full Width */}
|
||||
<div className="w-full py-12 sm:py-16 lg:py-20">
|
||||
<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">
|
||||
{/* Search Section */}
|
||||
<div className="mb-12 sm:mb-16">
|
||||
<div className="flex flex-col lg:flex-row gap-6 mb-8">
|
||||
<div className="flex-1 relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#d4af37]/10 to-transparent rounded-xl blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-5 top-1/2 transform -translate-y-1/2 text-[#d4af37] w-5 h-5 z-10" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search blog posts..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="w-full pl-14 pr-5 py-4 bg-gradient-to-br from-[#1a1a1a] to-[#0f0f0f] border border-[#d4af37]/20 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37]/50 transition-all duration-300 backdrop-blur-sm font-light"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Layout: Posts on Left, Tags on Right */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12">
|
||||
{/* Blog Posts Section */}
|
||||
<div className="lg:col-span-9">
|
||||
|
||||
{/* Results Count */}
|
||||
{!loading && total > 0 && (
|
||||
<div className="mb-8 text-gray-400 font-light text-sm">
|
||||
Showing {posts.length} of {total} posts
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Blog Posts Grid - Luxury Design */}
|
||||
{posts.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-[#d4af37]/10 mb-6">
|
||||
<BookOpen className="w-10 h-10 text-[#d4af37]" />
|
||||
</div>
|
||||
<p className="text-gray-400 text-xl font-light">No blog posts found</p>
|
||||
<p className="text-gray-500 text-sm mt-2">Try adjusting your search or filters</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 lg:gap-10 mb-12">
|
||||
{posts.map((post, index) => (
|
||||
<Link
|
||||
key={post.id}
|
||||
to={`/blog/${post.slug}`}
|
||||
className="group relative bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[#d4af37]/20 overflow-hidden hover:border-[#d4af37]/60 transition-all duration-700 hover:shadow-2xl hover:shadow-[#d4af37]/30 hover:-translate-y-3"
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
{/* Premium Glow Effects */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37]/0 via-[#d4af37]/0 to-[#d4af37]/0 group-hover:from-[#d4af37]/10 group-hover:via-[#d4af37]/5 group-hover:to-[#d4af37]/10 transition-all duration-700 rounded-3xl blur-2xl opacity-0 group-hover:opacity-100"></div>
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#d4af37]/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10">
|
||||
{post.featured_image && (
|
||||
<div className="relative h-72 sm:h-80 overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent z-10"></div>
|
||||
<img
|
||||
src={post.featured_image}
|
||||
alt={post.title}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-1000 ease-out"
|
||||
/>
|
||||
{/* Premium Badge Overlay */}
|
||||
<div className="absolute top-6 left-6 z-20">
|
||||
<div className="bg-gradient-to-br from-[#d4af37]/20 to-[#c9a227]/10 backdrop-blur-md rounded-2xl px-4 py-2 border border-[#d4af37]/40 shadow-xl">
|
||||
<div className="flex items-center gap-2 text-[#d4af37]">
|
||||
<Eye className="w-4 h-4" />
|
||||
<span className="text-sm font-semibold">{post.views_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Luxury Corner Accent */}
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-[#d4af37]/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-8">
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-5">
|
||||
{post.tags.slice(0, 2).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-1.5 bg-gradient-to-br from-[#d4af37]/15 to-[#d4af37]/5 text-[#d4af37] rounded-full text-xs font-semibold border border-[#d4af37]/30 backdrop-blur-sm shadow-lg"
|
||||
>
|
||||
<Tag className="w-3 h-3" />
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-2xl sm:text-3xl font-serif font-bold text-white mb-4 group-hover:text-[#d4af37] transition-colors duration-500 line-clamp-2 leading-tight tracking-tight">
|
||||
{post.title}
|
||||
</h2>
|
||||
{post.excerpt && (
|
||||
<p className="text-gray-300 text-base mb-6 line-clamp-3 font-light leading-relaxed">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center justify-between text-sm text-gray-400 pt-6 border-t border-[#d4af37]/20 mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
{post.published_at && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4 text-[#d4af37]" />
|
||||
<span className="font-light">{formatDate(post.published_at)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{post.author_name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-[#d4af37]" />
|
||||
<span className="font-light">{post.author_name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-4 border-t border-[#d4af37]/10">
|
||||
<div className="flex items-center gap-3 text-[#d4af37] group-hover:gap-4 transition-all duration-300">
|
||||
<span className="text-sm font-semibold tracking-wide uppercase">Read Article</span>
|
||||
<ArrowRight className="w-5 h-5 group-hover:translate-x-2 transition-transform duration-300" />
|
||||
</div>
|
||||
<div className="w-12 h-0.5 bg-gradient-to-r from-[#d4af37] to-transparent group-hover:w-20 transition-all duration-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-16 flex justify-center">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tags Sidebar - Right Side */}
|
||||
<div className="lg:col-span-3">
|
||||
{allTags.length > 0 && (
|
||||
<div className="sticky top-8">
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a] rounded-3xl border-2 border-[#d4af37]/20 p-8 backdrop-blur-xl shadow-2xl">
|
||||
<div className="flex items-center gap-3 mb-6 pb-6 border-b border-[#d4af37]/20">
|
||||
<div className="w-1 h-8 bg-gradient-to-b from-[#d4af37] to-[#c9a227] rounded-full"></div>
|
||||
<h3 className="text-xl font-serif font-bold text-white">Filter by Tags</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTag(null);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className={`group relative w-full text-left px-5 py-4 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
||||
selectedTag === null
|
||||
? 'bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] shadow-lg shadow-[#d4af37]/40'
|
||||
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[#d4af37]/20 hover:border-[#d4af37]/50 hover:text-[#d4af37] hover:bg-[#1a1a1a]'
|
||||
}`}
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<Tag className="w-4 h-4" />
|
||||
All Posts
|
||||
</span>
|
||||
{selectedTag === null && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent animate-shimmer"></div>
|
||||
)}
|
||||
</button>
|
||||
{allTags.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => {
|
||||
setSelectedTag(tag);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className={`group relative w-full text-left px-5 py-4 rounded-2xl text-sm font-medium transition-all duration-300 overflow-hidden ${
|
||||
selectedTag === tag
|
||||
? 'bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] shadow-lg shadow-[#d4af37]/40'
|
||||
: 'bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] text-gray-300 border-2 border-[#d4af37]/20 hover:border-[#d4af37]/50 hover:text-[#d4af37] hover:bg-[#1a1a1a]'
|
||||
}`}
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<Tag className="w-4 h-4" />
|
||||
{tag}
|
||||
</span>
|
||||
{selectedTag === tag && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent animate-shimmer"></div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogPage;
|
||||
@@ -1,180 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { XCircle, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
import { createSanitizedHtml } from '../utils/htmlSanitizer';
|
||||
|
||||
const CancellationPolicyPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getCancellationContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
// Process HTML content to ensure text is visible
|
||||
if (content.content) {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<XCircle className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Cancellation Policy</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<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-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<XCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</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-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Cancellation Policy'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
For questions about cancellations, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CancellationPolicyPage;
|
||||
|
||||
@@ -1,518 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Mail, Phone, MapPin, Send, User, MessageSquare } from 'lucide-react';
|
||||
import { submitContactForm } from '../services/api/contactService';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import { toast } from 'react-toastify';
|
||||
import Recaptcha from '../components/common/Recaptcha';
|
||||
import { recaptchaService } from '../services/api/systemSettingsService';
|
||||
import ChatWidget from '../components/chat/ChatWidget';
|
||||
import { useAntibotForm } from '../hooks/useAntibotForm';
|
||||
import HoneypotField from '../components/common/HoneypotField';
|
||||
|
||||
const ContactPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// Enhanced antibot protection
|
||||
const {
|
||||
honeypotValue,
|
||||
setHoneypotValue,
|
||||
recaptchaToken,
|
||||
setRecaptchaToken,
|
||||
validate: validateAntibot,
|
||||
rateLimitInfo,
|
||||
} = useAntibotForm({
|
||||
formId: 'contact',
|
||||
minTimeOnPage: 5000,
|
||||
minTimeToFill: 3000,
|
||||
requireRecaptcha: false,
|
||||
maxAttempts: 5,
|
||||
onValidationError: (errors) => {
|
||||
errors.forEach((err) => toast.error(err));
|
||||
},
|
||||
});
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = 'Name is required';
|
||||
}
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = 'Please enter a valid email address';
|
||||
}
|
||||
|
||||
if (!formData.subject.trim()) {
|
||||
newErrors.subject = 'Subject is required';
|
||||
}
|
||||
|
||||
if (!formData.message.trim()) {
|
||||
newErrors.message = 'Message is required';
|
||||
} else if (formData.message.trim().length < 10) {
|
||||
newErrors.message = 'Message must be at least 10 characters long';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate antibot protection
|
||||
const isValid = await validateAntibot();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (recaptchaToken) {
|
||||
try {
|
||||
const verifyResponse = await recaptchaService.verifyRecaptcha(recaptchaToken);
|
||||
if (verifyResponse.status === 'error' || !verifyResponse.data.verified) {
|
||||
toast.error('reCAPTCHA verification failed. Please try again.');
|
||||
setRecaptchaToken(null);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('reCAPTCHA verification failed. Please try again.');
|
||||
setRecaptchaToken(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await submitContactForm(formData);
|
||||
toast.success('Thank you for contacting us! We will get back to you soon.');
|
||||
|
||||
|
||||
setFormData({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
});
|
||||
setErrors({});
|
||||
setRecaptchaToken(null);
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.response?.data?.detail || error?.message || 'Failed to send message. Please try again.';
|
||||
toast.error(errorMessage);
|
||||
setRecaptchaToken(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
const response = await pageContentService.getContactContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
setPageContent(response.data.page_content);
|
||||
|
||||
|
||||
if (response.data.page_content.meta_title) {
|
||||
document.title = response.data.page_content.meta_title;
|
||||
}
|
||||
if (response.data.page_content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', response.data.page_content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
|
||||
const displayPhone = settings.company_phone || 'Available 24/7 for your convenience';
|
||||
const displayEmail = settings.company_email || "We'll respond within 24 hours";
|
||||
const displayAddress = settings.company_address || 'Visit us at our hotel reception';
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
|
||||
|
||||
if (errors[name]) {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[name];
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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="w-full bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#1a1a1a] border-b border-[#d4af37]/10 pt-6 sm:pt-7 md:pt-8 overflow-hidden relative">
|
||||
{}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-10 left-10 w-32 sm:w-48 h-32 sm:h-48 bg-[#d4af37] rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-10 right-10 w-40 sm:w-64 h-40 sm:h-64 bg-[#c9a227] rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[#d4af37]/50 to-transparent"></div>
|
||||
|
||||
<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 sm:py-5 md:py-6 relative z-10">
|
||||
<div className="max-w-2xl mx-auto text-center px-2">
|
||||
<div className="flex justify-center mb-2 sm:mb-3 md:mb-4">
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37] via-[#f5d76e] to-[#c9a227] 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-[#d4af37]/40 backdrop-blur-sm shadow-xl shadow-[#d4af37]/20 group-hover:border-[#d4af37]/60 transition-all duration-300">
|
||||
<Mail className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[#d4af37] drop-shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl xs:text-3xl sm:text-4xl md:text-5xl font-serif font-semibold mb-2 sm:mb-3 tracking-tight leading-tight px-2">
|
||||
<span className="bg-gradient-to-r from-white via-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent?.title || 'Contact Us'}
|
||||
</span>
|
||||
</h1>
|
||||
<div className="w-12 sm:w-16 md:w-20 h-0.5 bg-gradient-to-r from-transparent via-[#d4af37] 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">
|
||||
{pageContent?.subtitle || pageContent?.description || "Experience the pinnacle of hospitality. We're here to make your stay extraordinary."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[#d4af37]/30 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="w-full py-4 sm:py-6 md:py-8 lg:py-10 xl:py-12">
|
||||
<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">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 sm:gap-5 md:gap-6 lg:gap-7 xl:gap-8 2xl:gap-10 max-w-7xl mx-auto">
|
||||
{}
|
||||
<div className="lg:col-span-4">
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a]
|
||||
rounded-xl sm:rounded-2xl border-2 border-[#d4af37]/30 p-5 sm:p-6 md:p-8 lg:p-10
|
||||
shadow-2xl shadow-[#d4af37]/10 backdrop-blur-xl
|
||||
relative overflow-hidden h-full group hover:border-[#d4af37]/50 transition-all duration-500">
|
||||
{}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37]/5 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8">
|
||||
<div className="w-0.5 sm:w-1 h-6 sm:h-8 bg-gradient-to-b from-[#d4af37] to-[#c9a227] rounded-full"></div>
|
||||
<h2 className="text-xl sm:text-2xl md:text-3xl font-serif font-semibold
|
||||
text-white tracking-tight">
|
||||
Get in Touch
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 sm:space-y-6 md:space-y-7">
|
||||
<div className="flex items-start gap-3 sm:gap-4 md:gap-5 group/item hover:translate-x-1 transition-transform duration-300">
|
||||
<div className="p-2.5 sm:p-3 md:p-4 bg-gradient-to-br from-[#d4af37]/20 to-[#d4af37]/10 rounded-lg sm:rounded-xl border border-[#d4af37]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[#d4af37]/30 group-hover/item:to-[#d4af37]/20 group-hover/item:border-[#d4af37]/60 transition-all duration-300 shadow-lg shadow-[#d4af37]/10">
|
||||
<Mail className="w-5 h-5 sm:w-6 sm:h-6 text-[#d4af37] drop-shadow-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm sm:text-base font-medium text-[#d4af37] 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-[#d4af37] transition-colors">
|
||||
{displayEmail}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-[#d4af37]/20 to-[#d4af37]/10 rounded-lg sm:rounded-xl border border-[#d4af37]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[#d4af37]/30 group-hover/item:to-[#d4af37]/20 group-hover/item:border-[#d4af37]/60 transition-all duration-300 shadow-lg shadow-[#d4af37]/10">
|
||||
<Phone className="w-5 h-5 sm:w-6 sm:h-6 text-[#d4af37] drop-shadow-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm sm:text-base font-medium text-[#d4af37] 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-[#d4af37] transition-colors">
|
||||
{displayPhone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-[#d4af37]/20 to-[#d4af37]/10 rounded-lg sm:rounded-xl border border-[#d4af37]/40 flex-shrink-0 group-hover/item:bg-gradient-to-br group-hover/item:from-[#d4af37]/30 group-hover/item:to-[#d4af37]/20 group-hover/item:border-[#d4af37]/60 transition-all duration-300 shadow-lg shadow-[#d4af37]/10">
|
||||
<MapPin className="w-5 h-5 sm:w-6 sm:h-6 text-[#d4af37] drop-shadow-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm sm:text-base font-medium text-[#d4af37] 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">
|
||||
{displayAddress}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
{pageContent?.map_url && (
|
||||
<div className="mt-6 sm:mt-7 md:mt-8 pt-6 sm:pt-7 md:pt-8 border-t border-[#d4af37]/30">
|
||||
<h3 className="text-sm sm:text-base font-medium text-[#d4af37] mb-3 sm:mb-4 tracking-wide">
|
||||
Find Us
|
||||
</h3>
|
||||
<div className="relative rounded-lg overflow-hidden border-2 border-[#d4af37]/30 shadow-lg shadow-[#d4af37]/10 group hover:border-[#d4af37]/50 transition-all duration-300">
|
||||
<iframe
|
||||
src={pageContent.map_url}
|
||||
width="100%"
|
||||
height="200"
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
className="w-full h-40 sm:h-44 md:h-48 rounded-lg"
|
||||
title="Hotel Location"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-5 sm:mt-6 md:mt-7 pt-5 sm:pt-6 md:pt-7 border-t border-[#d4af37]/30">
|
||||
<p className="text-gray-400 font-light text-xs sm:text-sm leading-relaxed tracking-wide">
|
||||
{pageContent?.content || "Our team is here to help you with any questions about your stay, bookings, or special requests. We're committed to exceeding your expectations."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="lg:col-span-8">
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] via-[#0f0f0f] to-[#0a0a0a]
|
||||
rounded-xl sm:rounded-2xl border-2 border-[#d4af37]/30 p-5 sm:p-6 md:p-8 lg:p-10
|
||||
shadow-2xl shadow-[#d4af37]/10 backdrop-blur-xl
|
||||
relative overflow-hidden">
|
||||
{}
|
||||
<div className="absolute inset-0 opacity-5">
|
||||
<div className="absolute top-0 right-0 w-48 sm:w-64 md:w-96 h-48 sm:h-64 md:h-96 bg-[#d4af37] rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-2 sm:gap-3 mb-6 sm:mb-7 md:mb-8">
|
||||
<div className="w-0.5 sm:w-1 h-6 sm:h-8 bg-gradient-to-b from-[#d4af37] to-[#c9a227] rounded-full"></div>
|
||||
<h2 className="text-xl sm:text-2xl md:text-3xl font-serif font-semibold
|
||||
text-white tracking-tight">
|
||||
Send Us a Message
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5 sm:space-y-6 md:space-y-7 relative">
|
||||
{/* Honeypot field - hidden from users */}
|
||||
<HoneypotField value={honeypotValue} onChange={setHoneypotValue} />
|
||||
|
||||
{rateLimitInfo && !rateLimitInfo.allowed && (
|
||||
<div className="bg-yellow-900/50 backdrop-blur-sm border border-yellow-500/50 text-yellow-200 px-4 py-3 rounded-lg text-sm font-light mb-4">
|
||||
Too many contact form submissions. Please try again later.
|
||||
</div>
|
||||
)}
|
||||
{}
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||
<User className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[#d4af37] drop-shadow-lg" />
|
||||
Full Name <span className="text-[#d4af37] font-semibold">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
||||
focus:outline-none focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] focus:shadow-lg focus:shadow-[#d4af37]/20
|
||||
transition-all duration-300 hover:border-[#d4af37]/40
|
||||
${errors.name ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[#d4af37]/30'}`}
|
||||
placeholder="Enter your full name"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="mt-1.5 sm:mt-2 text-xs text-red-400 font-light">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-5 md:gap-6 lg:gap-7">
|
||||
{}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
||||
<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-[#d4af37] drop-shadow-lg" />
|
||||
Email <span className="text-[#d4af37] font-semibold">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
||||
focus:outline-none focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] focus:shadow-lg focus:shadow-[#d4af37]/20
|
||||
transition-all duration-300 hover:border-[#d4af37]/40
|
||||
${errors.email ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[#d4af37]/30'}`}
|
||||
placeholder="your.email@example.com"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1.5 sm:mt-2 text-xs text-red-400 font-light">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||
<Phone className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[#d4af37] drop-shadow-lg" />
|
||||
Phone <span className="text-gray-500 text-xs">(Optional)</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 border-[#d4af37]/30 rounded-lg
|
||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
||||
focus:outline-none focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] focus:shadow-lg focus:shadow-[#d4af37]/20
|
||||
transition-all duration-300 hover:border-[#d4af37]/40"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div>
|
||||
<label htmlFor="subject" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[#d4af37] drop-shadow-lg" />
|
||||
Subject <span className="text-[#d4af37] font-semibold">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
value={formData.subject}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
||||
text-white text-sm sm:text-base placeholder-gray-500/60
|
||||
focus:outline-none focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] focus:shadow-lg focus:shadow-[#d4af37]/20
|
||||
transition-all duration-300 hover:border-[#d4af37]/40
|
||||
${errors.subject ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[#d4af37]/30'}`}
|
||||
placeholder="What is this regarding?"
|
||||
/>
|
||||
{errors.subject && (
|
||||
<p className="mt-1.5 sm:mt-2 text-xs text-red-400 font-light">{errors.subject}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-xs sm:text-sm font-medium text-gray-300 mb-2 sm:mb-3 tracking-wide">
|
||||
<span className="flex items-center gap-1.5 sm:gap-2">
|
||||
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[#d4af37] drop-shadow-lg" />
|
||||
Message <span className="text-[#d4af37] font-semibold">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
rows={6}
|
||||
className={`w-full px-4 sm:px-5 py-3 sm:py-4 bg-gradient-to-br from-[#0f0f0f] to-[#0a0a0a] border-2 rounded-lg
|
||||
text-white text-sm sm:text-base placeholder-gray-500/60 resize-none
|
||||
focus:outline-none focus:ring-2 focus:ring-[#d4af37]/50 focus:border-[#d4af37] focus:shadow-lg focus:shadow-[#d4af37]/20
|
||||
transition-all duration-300 hover:border-[#d4af37]/40
|
||||
${errors.message ? 'border-red-500/50 focus:border-red-500 focus:ring-red-500/50' : 'border-[#d4af37]/30'}`}
|
||||
placeholder="Tell us more about your inquiry..."
|
||||
/>
|
||||
{errors.message && (
|
||||
<p className="mt-1.5 sm:mt-2 text-xs text-red-400 font-light">{errors.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="pt-2 sm:pt-3">
|
||||
<Recaptcha
|
||||
onChange={(token) => setRecaptchaToken(token)}
|
||||
onError={(error) => {
|
||||
console.error('reCAPTCHA error:', error);
|
||||
setRecaptchaToken(null);
|
||||
}}
|
||||
theme="dark"
|
||||
size="normal"
|
||||
className="flex justify-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="pt-2 sm:pt-3 md:pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group w-full sm:w-auto inline-flex items-center justify-center gap-2 sm:gap-3
|
||||
bg-gradient-to-r from-[#d4af37] via-[#f5d76e] to-[#d4af37]
|
||||
text-[#0f0f0f] font-semibold
|
||||
active:scale-[0.98]
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-all duration-500
|
||||
tracking-wide text-sm sm:text-base
|
||||
px-6 sm:px-8 md:px-10 py-3 sm:py-3.5 md:py-4 rounded-lg
|
||||
shadow-2xl shadow-[#d4af37]/40 hover:shadow-[#d4af37]/60 hover:scale-[1.02]
|
||||
relative overflow-hidden
|
||||
touch-manipulation min-h-[44px] sm:min-h-[48px] md:min-h-[52px]"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/30 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="w-4 h-4 sm:w-5 sm:h-5 border-2 border-[#0f0f0f] border-t-transparent rounded-full animate-spin relative z-10" />
|
||||
<span className="relative z-10">Sending...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<ChatWidget />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactPage;
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { HelpCircle, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
import { createSanitizedHtml } from '../utils/htmlSanitizer';
|
||||
|
||||
const FAQPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getFAQContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
if (content.content) {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<HelpCircle className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Frequently Asked Questions</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<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-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<HelpCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</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-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Frequently Asked Questions'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
Still have questions? Contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FAQPage;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,101 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Home, Hotel, AlertCircle } from 'lucide-react';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
|
||||
const NotFoundPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-gray-100 to-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative overflow-hidden">
|
||||
{/* Background Pattern */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-5"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 0h2v20H9zM25 0h2v20h-2zM41 0h2v20h-2zM57 0h2v20h-2zM0 9h20v2H0zM0 25h20v2H0zM0 41h20v2H0zM0 57h20v2H0zM40 9h20v2H40zM40 25h20v2H40zM40 41h20v2H40zM40 57h20v2H40zM9 40h2v20H9zM25 40h2v20h-2zM41 40h2v20h-2zM57 40h2v20h-2z' fill='%23d4af37' opacity='0.05'/%3E%3C/svg%3E")`
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="max-w-2xl w-full text-center relative z-10">
|
||||
{/* Logo */}
|
||||
<div className="flex justify-center mb-6 sm:mb-8">
|
||||
{settings.company_logo_url ? (
|
||||
<img
|
||||
src={settings.company_logo_url.startsWith('http')
|
||||
? settings.company_logo_url
|
||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`}
|
||||
alt={settings.company_name || 'Logo'}
|
||||
className="h-16 sm:h-20 lg:h-24 w-auto max-w-[200px] sm:max-w-[250px] object-contain"
|
||||
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
||||
/>
|
||||
) : (
|
||||
<div className="relative p-4 sm:p-5 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full shadow-lg shadow-[#d4af37]/30">
|
||||
<Hotel className="w-12 h-12 sm:w-16 sm:h-16 lg:w-20 lg:h-20 text-[#0f0f0f] relative z-10" />
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#f5d76e] to-[#d4af37] rounded-full blur-xl opacity-50"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tagline */}
|
||||
{settings.company_tagline && (
|
||||
<p className="text-xs sm:text-sm text-[#d4af37] uppercase tracking-[2px] sm:tracking-[3px] mb-3 sm:mb-4 font-light">
|
||||
{settings.company_tagline}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 404 Number */}
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h1 className="text-8xl sm:text-9xl lg:text-[12rem] font-display font-bold text-transparent bg-clip-text bg-gradient-to-r from-[#d4af37] via-[#f5d76e] to-[#d4af37] leading-none tracking-tight">
|
||||
404
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Error Icon */}
|
||||
<div className="flex justify-center mb-6 sm:mb-8">
|
||||
<div className="relative p-4 sm:p-5 bg-red-50 rounded-full border-2 border-red-200">
|
||||
<AlertCircle className="w-8 h-8 sm:w-10 sm:h-10 text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Message */}
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-elegant font-semibold text-gray-900 mb-3 sm:mb-4 tracking-tight">
|
||||
Page Not Found
|
||||
</h2>
|
||||
<p className="text-base sm:text-lg text-gray-600 font-light tracking-wide max-w-md mx-auto leading-relaxed">
|
||||
We're sorry, but the page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Additional Info */}
|
||||
<div className="mb-8 sm:mb-10">
|
||||
<p className="text-sm sm:text-base text-gray-500 font-light">
|
||||
The page you requested could not be found on our server.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Home Button */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center justify-center gap-2 px-8 py-3.5 sm:px-10 sm:py-4 bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] font-semibold text-sm sm:text-base rounded-lg shadow-lg shadow-[#d4af37]/30 hover:from-[#f5d76e] hover:to-[#d4af37] hover:shadow-xl hover:shadow-[#d4af37]/40 transition-all duration-300 ease-out hover:-translate-y-0.5 active:translate-y-0 relative overflow-hidden group"
|
||||
>
|
||||
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
||||
<Home className="w-5 h-5 sm:w-6 sm:h-6 relative z-10" />
|
||||
<span className="relative z-10 tracking-wide">Back to Homepage</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Decorative Elements */}
|
||||
<div className="mt-12 sm:mt-16 flex justify-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-[#d4af37]/30"></div>
|
||||
<div className="w-2 h-2 rounded-full bg-[#d4af37]/50"></div>
|
||||
<div className="w-2 h-2 rounded-full bg-[#d4af37]/30"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFoundPage;
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Shield, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
import { createSanitizedHtml } from '../utils/htmlSanitizer';
|
||||
|
||||
const PrivacyPolicyPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getPrivacyContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
// Process HTML content to ensure text is visible
|
||||
if (content.content) {
|
||||
// Create a temporary div to parse HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
// Add color styles to elements that don't have them
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
// Only add color if not already set
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update content with processed HTML
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Shield className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Privacy Policy</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
{/* Back Link */}
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<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-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<Shield className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</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-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Privacy Policy'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer Note */}
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
For questions about this policy, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacyPolicyPage;
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { RefreshCw, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
import { createSanitizedHtml } from '../utils/htmlSanitizer';
|
||||
|
||||
const RefundsPolicyPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getRefundsContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
// Process HTML content to ensure text is visible
|
||||
if (content.content) {
|
||||
// Create a temporary div to parse HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
// Add color styles to elements that don't have them
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
// Only add color if not already set
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update content with processed HTML
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<RefreshCw className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Refunds Policy</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
{/* Back Link */}
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<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-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<RefreshCw className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</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-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Refunds Policy'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer Note */}
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
For refund inquiries, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RefundsPolicyPage;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { SidebarStaff } from '../components/layout';
|
||||
import StaffChatNotification from '../components/chat/StaffChatNotification';
|
||||
import { ChatNotificationProvider } from '../contexts/ChatNotificationContext';
|
||||
import { useResponsive } from '../hooks';
|
||||
import SidebarStaff from '../shared/components/SidebarStaff';
|
||||
import StaffChatNotification from '../features/notifications/components/StaffChatNotification';
|
||||
import { ChatNotificationProvider } from '../features/notifications/contexts/ChatNotificationContext';
|
||||
import { useResponsive } from '../shared/hooks/useResponsive';
|
||||
|
||||
const StaffLayout: React.FC = () => {
|
||||
const { isMobile } = useResponsive();
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Scale, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
import { createSanitizedHtml } from '../utils/htmlSanitizer';
|
||||
|
||||
const TermsPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getTermsContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
// Process HTML content to ensure text is visible
|
||||
if (content.content) {
|
||||
// Create a temporary div to parse HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
// Add color styles to elements that don't have them
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
// Only add color if not already set
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update content with processed HTML
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Scale className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Terms & Conditions</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
{/* Back Link */}
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<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-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<Scale className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</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-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Terms & Conditions'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={createSanitizedHtml(
|
||||
pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer Note */}
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
For questions about these terms, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsPage;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '../../test/utils/test-utils';
|
||||
import HomePage from '../HomePage';
|
||||
import HomePage from '../../features/content/pages/HomePage';
|
||||
|
||||
// Mock the components that might cause issues
|
||||
vi.mock('../../components/rooms/BannerCarousel', () => ({
|
||||
vi.mock('../../features/rooms/components/BannerCarousel', () => ({
|
||||
default: ({ children, banners }: any) => (
|
||||
<div data-testid="banner-carousel">
|
||||
{banners.map((banner: any) => (
|
||||
@@ -16,7 +16,7 @@ vi.mock('../../components/rooms/BannerCarousel', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../components/rooms/SearchRoomForm', () => ({
|
||||
vi.mock('../../features/rooms/components/SearchRoomForm', () => ({
|
||||
default: ({ className }: any) => (
|
||||
<div data-testid="search-room-form" className={className}>
|
||||
Search Form
|
||||
@@ -24,7 +24,7 @@ vi.mock('../../components/rooms/SearchRoomForm', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../components/rooms/RoomCarousel', () => ({
|
||||
vi.mock('../../features/rooms/components/RoomCarousel', () => ({
|
||||
default: ({ rooms }: any) => (
|
||||
<div data-testid="room-carousel">
|
||||
{rooms.map((room: any) => (
|
||||
@@ -36,11 +36,11 @@ vi.mock('../../components/rooms/RoomCarousel', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../components/rooms/BannerSkeleton', () => ({
|
||||
vi.mock('../../features/rooms/components/BannerSkeleton', () => ({
|
||||
default: () => <div data-testid="banner-skeleton">Loading banners...</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../../components/rooms/RoomCardSkeleton', () => ({
|
||||
vi.mock('../../features/rooms/components/RoomCardSkeleton', () => ({
|
||||
default: () => <div data-testid="room-card-skeleton">Loading room...</div>,
|
||||
}));
|
||||
|
||||
|
||||
@@ -29,14 +29,17 @@ import {
|
||||
Award
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState, ExportButton } from '../../components/common';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import { reportService, ReportData, reviewService, Review } from '../../services/api';
|
||||
import { auditService, AuditLog, AuditLogFilters } from '../../services/api/auditService';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import ExportButton from '../../shared/components/ExportButton';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import reportService, { ReportData } from '../../features/analytics/services/reportService';
|
||||
import reviewService, { Review } from '../../features/reviews/services/reviewService';
|
||||
import { auditService, AuditLog, AuditLogFilters } from '../../features/analytics/services/auditService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import analyticsService, {
|
||||
ComprehensiveAnalyticsData,
|
||||
RevPARData,
|
||||
@@ -53,10 +56,10 @@ import analyticsService, {
|
||||
ProfitLossData,
|
||||
PaymentMethodAnalyticsData,
|
||||
RefundAnalysisData,
|
||||
} from '../../services/api/analyticsService';
|
||||
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../components/analytics/SimpleChart';
|
||||
import { exportData } from '../../utils/exportUtils';
|
||||
import CustomReportBuilder from '../../components/analytics/CustomReportBuilder';
|
||||
} from '../../features/analytics/services/analyticsService';
|
||||
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
|
||||
import { exportData } from '../../shared/utils/exportUtils';
|
||||
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
|
||||
|
||||
type AnalyticsTab = 'overview' | 'reports' | 'revenue' | 'operational' | 'guest' | 'financial' | 'audit-logs' | 'reviews';
|
||||
|
||||
|
||||
@@ -7,14 +7,18 @@ import {
|
||||
DollarSign,
|
||||
AlertCircle
|
||||
} from 'lucide-react';
|
||||
import { reportService, ReportData, paymentService, invoiceService } from '../../services/api';
|
||||
import type { Payment } from '../../services/api/paymentService';
|
||||
import type { Invoice } from '../../services/api/invoiceService';
|
||||
import reportService, { ReportData } from '../../features/analytics/services/reportService';
|
||||
import paymentService from '../../features/payments/services/paymentService';
|
||||
import invoiceService from '../../features/payments/services/invoiceService';
|
||||
import type { Payment } from '../../features/payments/services/paymentService';
|
||||
import type { Invoice } from '../../features/payments/services/invoiceService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState, ExportButton } from '../../components/common';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import ExportButton from '../../shared/components/ExportButton';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const AccountantDashboardPage: React.FC = () => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Search, Plus, Edit, Trash2, Eye, FileText, Filter } from 'lucide-react';
|
||||
import { invoiceService, Invoice } from '../../services/api';
|
||||
import invoiceService, { Invoice } from '../../features/payments/services/invoiceService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { ExportButton } from '../../components/common';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import ExportButton from '../../shared/components/ExportButton';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
|
||||
const InvoiceManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Search } from 'lucide-react';
|
||||
import { paymentService } from '../../services/api';
|
||||
import type { Payment } from '../../services/api/paymentService';
|
||||
import paymentService from '../../features/payments/services/paymentService';
|
||||
import type { Payment } from '../../features/payments/services/paymentService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { ExportButton } from '../../components/common';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import ExportButton from '../../shared/components/ExportButton';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
|
||||
const PaymentManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -17,9 +17,9 @@ import {
|
||||
Plus,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading } from '../../components/common';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import analyticsService, {
|
||||
ComprehensiveAnalyticsData,
|
||||
RevPARData,
|
||||
@@ -36,10 +36,10 @@ import analyticsService, {
|
||||
ProfitLossData,
|
||||
PaymentMethodAnalyticsData,
|
||||
RefundAnalysisData,
|
||||
} from '../../services/api/analyticsService';
|
||||
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../components/analytics/SimpleChart';
|
||||
import { exportData } from '../../utils/exportUtils';
|
||||
import CustomReportBuilder from '../../components/analytics/CustomReportBuilder';
|
||||
} from '../../features/analytics/services/analyticsService';
|
||||
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
|
||||
import { exportData } from '../../shared/utils/exportUtils';
|
||||
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
|
||||
|
||||
type AnalyticsCategory = 'revenue' | 'operational' | 'guest' | 'financial' | 'comprehensive';
|
||||
|
||||
|
||||
@@ -22,18 +22,18 @@ import {
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import advancedRoomService, {
|
||||
RoomStatusBoardItem,
|
||||
} from '../../services/api/advancedRoomService';
|
||||
import { roomService, Room } from '../../services/api';
|
||||
import MaintenanceManagement from '../../components/shared/MaintenanceManagement';
|
||||
import HousekeepingManagement from '../../components/shared/HousekeepingManagement';
|
||||
import InspectionManagement from '../../components/shared/InspectionManagement';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import apiClient from '../../services/api/apiClient';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { logger } from '../../utils/logger';
|
||||
} from '../../features/rooms/services/advancedRoomService';
|
||||
import roomService, { Room } from '../../features/rooms/services/roomService';
|
||||
import MaintenanceManagement from '../../features/hotel_services/components/MaintenanceManagement';
|
||||
import HousekeepingManagement from '../../features/hotel_services/components/HousekeepingManagement';
|
||||
import InspectionManagement from '../../features/hotel_services/components/InspectionManagement';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import apiClient from '../../shared/services/apiClient';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
|
||||
type Tab = 'status-board' | 'maintenance' | 'housekeeping' | 'inspections' | 'rooms';
|
||||
|
||||
|
||||
@@ -29,15 +29,18 @@ import {
|
||||
Award
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState, ExportButton } from '../../components/common';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import { reportService, ReportData, reviewService, Review } from '../../services/api';
|
||||
import { auditService, AuditLog, AuditLogFilters } from '../../services/api/auditService';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { logger } from '../../utils/logger';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import ExportButton from '../../shared/components/ExportButton';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import reportService, { ReportData } from '../../features/analytics/services/reportService';
|
||||
import reviewService, { Review } from '../../features/reviews/services/reviewService';
|
||||
import { auditService, AuditLog, AuditLogFilters } from '../../features/analytics/services/auditService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import analyticsService, {
|
||||
ComprehensiveAnalyticsData,
|
||||
RevPARData,
|
||||
@@ -54,10 +57,10 @@ import analyticsService, {
|
||||
ProfitLossData,
|
||||
PaymentMethodAnalyticsData,
|
||||
RefundAnalysisData,
|
||||
} from '../../services/api/analyticsService';
|
||||
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../components/analytics/SimpleChart';
|
||||
import { exportData } from '../../utils/exportUtils';
|
||||
import CustomReportBuilder from '../../components/analytics/CustomReportBuilder';
|
||||
} from '../../features/analytics/services/analyticsService';
|
||||
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
|
||||
import { exportData } from '../../shared/utils/exportUtils';
|
||||
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
|
||||
|
||||
type AnalyticsTab = 'overview' | 'reports' | 'revenue' | 'operational' | 'guest' | 'financial' | 'audit-logs' | 'reviews';
|
||||
|
||||
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { auditService, AuditLog, AuditLogFilters } from '../../services/api/auditService';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { auditService, AuditLog, AuditLogFilters } from '../../features/analytics/services/auditService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
|
||||
const AuditLogsPage: React.FC = () => {
|
||||
const [logs, setLogs] = useState<AuditLog[]>([]);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Plus, Search, Edit, Trash2, X, Image as ImageIcon, Eye, EyeOff, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { ConfirmationDialog } from '../../components/common';
|
||||
import bannerServiceModule from '../../services/api/bannerService';
|
||||
import type { Banner } from '../../services/api/bannerService';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
|
||||
import bannerServiceModule from '../../features/content/services/bannerService';
|
||||
import type { Banner } from '../../features/content/services/bannerService';
|
||||
|
||||
const {
|
||||
getAllBanners,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Plus, Search, Edit, Trash2, X, Eye, EyeOff, Loader2, Calendar, User, Tag, GripVertical, Image as ImageIcon, Type, Quote, List, Video, ArrowRight, MoveUp, MoveDown, Sparkles, Upload } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { ConfirmationDialog } from '../../components/common';
|
||||
import { blogService, BlogPost, BlogPostCreate, BlogPostUpdate, BlogSection } from '../../services/api';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
|
||||
import blogService, { BlogPost, BlogPostCreate, BlogPostUpdate, BlogSection } from '../../features/content/services/blogService';
|
||||
|
||||
const BlogManagementPage: React.FC = () => {
|
||||
const [posts, setPosts] = useState<BlogPost[]>([]);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Search, Eye, XCircle, CheckCircle, Loader2, FileText, Plus, Mail } from 'lucide-react';
|
||||
import { bookingService, Booking, invoiceService } from '../../services/api';
|
||||
import bookingService, { Booking } from '../../features/bookings/services/bookingService';
|
||||
import invoiceService from '../../features/payments/services/invoiceService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import CreateBookingModal from '../../components/shared/CreateBookingModal';
|
||||
import { logger } from '../../utils/logger';
|
||||
import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
|
||||
const BookingManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -14,16 +14,17 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { useCurrency } from '../../contexts/CurrencyContext';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { useCurrency } from '../../features/payments/contexts/CurrencyContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { invoiceService, Invoice } from '../../services/api';
|
||||
import { paymentService } from '../../services/api';
|
||||
import type { Payment } from '../../services/api/paymentService';
|
||||
import { promotionService, Promotion } from '../../services/api';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import invoiceService, { Invoice } from '../../features/payments/services/invoiceService';
|
||||
import paymentService from '../../features/payments/services/paymentService';
|
||||
import type { Payment } from '../../features/payments/services/paymentService';
|
||||
import promotionService, { Promotion } from '../../features/loyalty/services/promotionService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
|
||||
type BusinessTab = 'overview' | 'invoices' | 'payments' | 'promotions';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, User, Hotel, CheckCircle, AlertCircle, Calendar, LogIn, LogOut } from 'lucide-react';
|
||||
import { bookingService, Booking } from '../../services/api';
|
||||
import bookingService, { Booking } from '../../features/bookings/services/bookingService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
|
||||
interface GuestInfo {
|
||||
name: string;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Search, FileText, CreditCard, Printer, CheckCircle } from 'lucide-react';
|
||||
import { bookingService, Booking } from '../../services/api';
|
||||
import bookingService, { Booking } from '../../features/bookings/services/bookingService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
|
||||
interface ServiceItem {
|
||||
service_name: string;
|
||||
|
||||
@@ -6,8 +6,8 @@ import adminPrivacyService, {
|
||||
CookieIntegrationSettingsResponse,
|
||||
CookiePolicySettings,
|
||||
CookiePolicySettingsResponse,
|
||||
} from '../../services/api/adminPrivacyService';
|
||||
import { Loading } from '../../components/common';
|
||||
} from '../../features/content/services/adminPrivacyService';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
|
||||
const CookieSettingsPage: React.FC = () => {
|
||||
const [policy, setPolicy] = useState<CookiePolicySettings>({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DollarSign, Save, Info, Globe } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import systemSettingsService, { PlatformCurrencyResponse } from '../../services/api/systemSettingsService';
|
||||
import { useCurrency } from '../../contexts/CurrencyContext';
|
||||
import { Loading } from '../../components/common';
|
||||
import { getCurrencySymbol } from '../../utils/format';
|
||||
import systemSettingsService, { PlatformCurrencyResponse } from '../../features/system/services/systemSettingsService';
|
||||
import { useCurrency } from '../../features/payments/contexts/CurrencyContext';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { getCurrencySymbol } from '../../shared/utils/format';
|
||||
|
||||
const CurrencySettingsPage: React.FC = () => {
|
||||
const { currency, supportedCurrencies, refreshCurrency } = useCurrency();
|
||||
|
||||
@@ -9,17 +9,19 @@ import {
|
||||
CreditCard,
|
||||
LogOut
|
||||
} from 'lucide-react';
|
||||
import { reportService, ReportData, paymentService } from '../../services/api';
|
||||
import type { Payment } from '../../services/api/paymentService';
|
||||
import reportService, { ReportData } from '../../features/analytics/services/reportService';
|
||||
import paymentService from '../../features/payments/services/paymentService';
|
||||
import type { Payment } from '../../features/payments/services/paymentService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
|
||||
const DashboardPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
Layers,
|
||||
Target
|
||||
} from 'lucide-react';
|
||||
import { emailCampaignService, Campaign, CampaignSegment, EmailTemplate, DripSequence, CampaignAnalytics } from '../../services/api/emailCampaignService';
|
||||
import { emailCampaignService, Campaign, CampaignSegment, EmailTemplate, DripSequence, CampaignAnalytics } from '../../features/notifications/services/emailCampaignService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
|
||||
type CampaignTab = 'campaigns' | 'segments' | 'templates' | 'drip-sequences' | 'analytics';
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Search, Eye, XCircle, CheckCircle, Loader2, Users, Plus } from 'lucide-react';
|
||||
import { groupBookingService, GroupBooking } from '../../services/api';
|
||||
import groupBookingService, { GroupBooking } from '../../features/bookings/services/groupBookingService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import CreateGroupBookingModal from '../../components/shared/CreateGroupBookingModal';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import CreateGroupBookingModal from '../../features/hotel_services/components/CreateGroupBookingModal';
|
||||
|
||||
const GroupBookingManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
Phone,
|
||||
MapPin,
|
||||
} from 'lucide-react';
|
||||
import { guestProfileService, GuestProfile, GuestListItem, GuestTag, GuestSegment, GuestSearchParams, GuestPreference } from '../../services/api';
|
||||
import guestProfileService, { GuestProfile, GuestListItem, GuestTag, GuestSegment, GuestSearchParams, GuestPreference } from '../../features/guest_management/services/guestProfileService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
|
||||
type TabType = 'list' | 'profile';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { ArrowLeft, Save } from 'lucide-react';
|
||||
import { invoiceService, Invoice, UpdateInvoiceData } from '../../services/api';
|
||||
import invoiceService, { Invoice, UpdateInvoiceData } from '../../features/payments/services/invoiceService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
|
||||
const InvoiceEditPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Search, Plus, Edit, Trash2, Eye, FileText, Filter } from 'lucide-react';
|
||||
import { invoiceService, Invoice } from '../../services/api';
|
||||
import invoiceService, { Invoice } from '../../features/payments/services/invoiceService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { ExportButton } from '../../components/common';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import ExportButton from '../../shared/components/ExportButton';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
|
||||
const InvoiceManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
Save
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState, ConfirmationDialog } from '../../components/common';
|
||||
import loyaltyService, { LoyaltyTier, LoyaltyReward } from '../../services/api/loyaltyService';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
|
||||
import loyaltyService, { LoyaltyTier, LoyaltyReward } from '../../features/loyalty/services/loyaltyService';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
|
||||
type Tab = 'users' | 'tiers' | 'rewards';
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@ import {
|
||||
Clock,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import notificationService, { Notification } from '../../services/api/notificationService';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import SendNotificationModal from '../../components/notifications/SendNotificationModal';
|
||||
import NotificationTemplatesModal from '../../components/notifications/NotificationTemplatesModal';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import notificationService, { Notification } from '../../features/notifications/services/notificationService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import SendNotificationModal from '../../features/notifications/components/SendNotificationModal';
|
||||
import NotificationTemplatesModal from '../../features/notifications/components/NotificationTemplatesModal';
|
||||
|
||||
const NotificationManagementPage: React.FC = () => {
|
||||
const [showSendModal, setShowSendModal] = useState(false);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Plus, Search, Edit, Trash2, X, Package as PackageIcon } from 'lucide-react';
|
||||
import { packageService, Package, PackageStatus, PackageItem, PackageItemType, CreatePackageData } from '../../services/api';
|
||||
import { roomService, Room } from '../../services/api';
|
||||
import { serviceService, Service } from '../../services/api';
|
||||
import packageService, { Package, PackageStatus, PackageItem, PackageItemType, CreatePackageData } from '../../features/loyalty/services/packageService';
|
||||
import roomService, { Room } from '../../features/rooms/services/roomService';
|
||||
import serviceService, { Service } from '../../features/hotel_services/services/serviceService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
|
||||
const PackageManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -23,11 +23,12 @@ import {
|
||||
Accessibility,
|
||||
HelpCircle
|
||||
} from 'lucide-react';
|
||||
import { pageContentService, PageContent, PageType, UpdatePageContentData, bannerService, Banner } from '../../services/api';
|
||||
import pageContentService, { PageContent, PageType, UpdatePageContentData } from '../../features/content/services/pageContentService';
|
||||
import bannerService, { Banner } from '../../features/content/services/bannerService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import { ConfirmationDialog } from '../../components/common';
|
||||
import IconPicker from '../../components/admin/IconPicker';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
|
||||
import IconPicker from '../../features/system/components/IconPicker';
|
||||
|
||||
type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo' | 'privacy' | 'terms' | 'refunds' | 'cancellation' | 'accessibility' | 'faq';
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Search } from 'lucide-react';
|
||||
import { paymentService } from '../../services/api';
|
||||
import type { Payment } from '../../services/api/paymentService';
|
||||
import paymentService from '../../features/payments/services/paymentService';
|
||||
import type { Payment } from '../../features/payments/services/paymentService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { ExportButton } from '../../components/common';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import ExportButton from '../../shared/components/ExportButton';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
|
||||
const PaymentManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Plus, Search, Edit, Trash2, X, Tag } from 'lucide-react';
|
||||
import { promotionService, Promotion } from '../../services/api';
|
||||
import promotionService, { Promotion } from '../../features/loyalty/services/promotionService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { useCurrency } from '../../contexts/CurrencyContext';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { useCurrency } from '../../features/payments/contexts/CurrencyContext';
|
||||
|
||||
const PromotionManagementPage: React.FC = () => {
|
||||
const { currency } = useCurrency();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Plus, Search, Edit, Trash2, X, Tag } from 'lucide-react';
|
||||
import { ratePlanService, RatePlan, RatePlanType, RatePlanStatus, CreateRatePlanData } from '../../services/api';
|
||||
import { roomService, Room } from '../../services/api';
|
||||
import ratePlanService, { RatePlan, RatePlanType, RatePlanStatus, CreateRatePlanData } from '../../features/rooms/services/ratePlanService';
|
||||
import roomService, { Room } from '../../features/rooms/services/roomService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
|
||||
const RatePlanManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -22,14 +22,15 @@ import {
|
||||
Calendar,
|
||||
Wrench
|
||||
} from 'lucide-react';
|
||||
import { bookingService, Booking, serviceService, Service } from '../../services/api';
|
||||
import bookingService, { Booking } from '../../features/bookings/services/bookingService';
|
||||
import serviceService, { Service } from '../../features/hotel_services/services/serviceService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import CreateBookingModal from '../../components/shared/CreateBookingModal';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal';
|
||||
|
||||
type ReceptionTab = 'overview' | 'check-in' | 'check-out' | 'bookings' | 'services';
|
||||
|
||||
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
PieChart
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import { reportService, ReportData } from '../../services/api';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import reportService, { ReportData } from '../../features/analytics/services/reportService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
|
||||
const ReportsPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { reviewService, Review } from '../../services/api';
|
||||
import reviewService, { Review } from '../../features/reviews/services/reviewService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
|
||||
const ReviewManagementPage: React.FC = () => {
|
||||
const [reviews, setReviews] = useState<Review[]>([]);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Plus, Search, Edit, Trash2, X, Upload, Image as ImageIcon, Check } from 'lucide-react';
|
||||
import { roomService, Room } from '../../services/api';
|
||||
import roomService, { Room } from '../../features/rooms/services/roomService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import apiClient from '../../services/api/apiClient';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import apiClient from '../../shared/services/apiClient';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
|
||||
const RoomManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -14,11 +14,11 @@ import {
|
||||
AlertCircle,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import { securityService, SecurityEvent, SecurityStats, OAuthProvider, DataSubjectRequest } from '../../services/api/securityService';
|
||||
import { securityService, SecurityEvent, SecurityStats, OAuthProvider, DataSubjectRequest } from '../../features/security/services/securityService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
|
||||
type SecurityTab = 'events' | 'stats' | 'ip-whitelist' | 'ip-blacklist' | 'oauth' | 'gdpr' | 'scan';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Plus, Search, Edit, Trash2, X } from 'lucide-react';
|
||||
import { serviceService, Service } from '../../services/api';
|
||||
import serviceService, { Service } from '../../features/hotel_services/services/serviceService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
|
||||
const ServiceManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -29,7 +29,7 @@ import adminPrivacyService, {
|
||||
CookieIntegrationSettings,
|
||||
CookiePolicySettings,
|
||||
CookiePolicySettingsResponse,
|
||||
} from '../../services/api/adminPrivacyService';
|
||||
} from '../../features/content/services/adminPrivacyService';
|
||||
import systemSettingsService, {
|
||||
StripeSettingsResponse,
|
||||
UpdateStripeSettingsRequest,
|
||||
@@ -41,11 +41,11 @@ import systemSettingsService, {
|
||||
UpdateSmtpSettingsRequest,
|
||||
CompanySettingsResponse,
|
||||
UpdateCompanySettingsRequest,
|
||||
} from '../../services/api/systemSettingsService';
|
||||
import { recaptchaService, RecaptchaSettingsAdminResponse, UpdateRecaptchaSettingsRequest } from '../../services/api/systemSettingsService';
|
||||
import { useCurrency } from '../../contexts/CurrencyContext';
|
||||
import { Loading } from '../../components/common';
|
||||
import { getCurrencySymbol } from '../../utils/format';
|
||||
} from '../../features/system/services/systemSettingsService';
|
||||
import { recaptchaService, RecaptchaSettingsAdminResponse, UpdateRecaptchaSettingsRequest } from '../../features/system/services/systemSettingsService';
|
||||
import { useCurrency } from '../../features/payments/contexts/CurrencyContext';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { getCurrencySymbol } from '../../shared/utils/format';
|
||||
|
||||
type SettingsTab = 'general' | 'cookie' | 'currency' | 'payment' | 'smtp' | 'company' | 'recaptcha';
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { toast } from 'react-toastify';
|
||||
import systemSettingsService, {
|
||||
StripeSettingsResponse,
|
||||
UpdateStripeSettingsRequest,
|
||||
} from '../../services/api/systemSettingsService';
|
||||
import { Loading } from '../../components/common';
|
||||
} from '../../features/system/services/systemSettingsService';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
|
||||
const StripeSettingsPage: React.FC = () => {
|
||||
const [settings, setSettings] = useState<StripeSettingsResponse['data'] | null>(null);
|
||||
|
||||
@@ -11,14 +11,15 @@ import {
|
||||
Play,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import taskService, { Task, TaskStatistics } from '../../services/api/taskService';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import TaskDetailModal from '../../components/tasks/TaskDetailModal';
|
||||
import CreateTaskModal from '../../components/tasks/CreateTaskModal';
|
||||
import TaskFilters from '../../components/tasks/TaskFilters';
|
||||
import { logger } from '../../utils/logger';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import taskService, { Task, TaskStatistics } from '../../features/system/services/taskService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import TaskDetailModal from '../../features/system/components/TaskDetailModal';
|
||||
import CreateTaskModal from '../../features/system/components/CreateTaskModal';
|
||||
import TaskFilters from '../../features/system/components/TaskFilters';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
|
||||
type TaskStatus = 'pending' | 'assigned' | 'in_progress' | 'completed' | 'cancelled' | 'overdue';
|
||||
type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Plus, Search, Edit, Trash2, X } from 'lucide-react';
|
||||
import { userService, User } from '../../services/api';
|
||||
import userService, { User } from '../../features/auth/services/userService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import LoadingButton from '../../components/common/LoadingButton';
|
||||
import ErrorMessage from '../../components/common/ErrorMessage';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import LoadingButton from '../../shared/components/LoadingButton';
|
||||
import ErrorMessage from '../../shared/components/ErrorMessage';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { useApiCall } from '../../hooks/useApiCall';
|
||||
import { logger } from '../../shared/utils/logger';
|
||||
import { useApiCall } from '../../shared/hooks/useApiCall';
|
||||
|
||||
const UserManagementPage: React.FC = () => {
|
||||
const { userInfo } = useAuthStore();
|
||||
|
||||
@@ -9,11 +9,12 @@ import {
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import workflowService, { Workflow } from '../../services/api/workflowService';
|
||||
import WorkflowBuilder from '../../components/workflows/WorkflowBuilder';
|
||||
import WorkflowDetailModal from '../../components/workflows/WorkflowDetailModal';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import workflowService, { Workflow } from '../../features/system/services/workflowService';
|
||||
import WorkflowBuilder from '../../features/system/components/WorkflowBuilder';
|
||||
import WorkflowDetailModal from '../../features/system/components/WorkflowDetailModal';
|
||||
|
||||
const WorkflowManagementPage: React.FC = () => {
|
||||
const [showBuilder, setShowBuilder] = useState(false);
|
||||
|
||||
@@ -26,15 +26,14 @@ import { toast } from 'react-toastify';
|
||||
import {
|
||||
getBookingById,
|
||||
type Booking,
|
||||
} from '../../services/api/bookingService';
|
||||
} from '../../features/bookings/services/bookingService';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import PaymentStatusBadge from
|
||||
'../../components/common/PaymentStatusBadge';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import CancelBookingModal from '../../components/booking/CancelBookingModal';
|
||||
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import PaymentStatusBadge from '../../shared/components/PaymentStatusBadge';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
import CancelBookingModal from '../../features/bookings/components/CancelBookingModal';
|
||||
|
||||
const BookingDetailPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Calendar, Clock } from 'lucide-react';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
|
||||
const BookingListPage: React.FC = () => {
|
||||
return (
|
||||
|
||||
@@ -27,13 +27,12 @@ import {
|
||||
getBookingById,
|
||||
generateQRCode,
|
||||
type Booking,
|
||||
} from '../../services/api/bookingService';
|
||||
import { confirmBankTransfer } from
|
||||
'../../services/api/paymentService';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import DepositPaymentModal from '../../components/payments/DepositPaymentModal';
|
||||
} from '../../features/bookings/services/bookingService';
|
||||
import { confirmBankTransfer } from '../../features/payments/services/paymentService';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
import DepositPaymentModal from '../../features/payments/components/DepositPaymentModal';
|
||||
|
||||
const BookingSuccessPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { confirmBoricaPayment } from '../../services/api/paymentService';
|
||||
import { confirmBoricaPayment } from '../../features/payments/services/paymentService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
||||
|
||||
|
||||
@@ -8,15 +8,16 @@ import {
|
||||
CreditCard,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import dashboardService, { CustomerDashboardStats } from '../../services/api/dashboardService';
|
||||
import { paymentService } from '../../services/api';
|
||||
import type { Payment } from '../../services/api/paymentService';
|
||||
import dashboardService, { CustomerDashboardStats } from '../../features/analytics/services/dashboardService';
|
||||
import paymentService from '../../features/payments/services/paymentService';
|
||||
import type { Payment } from '../../features/payments/services/paymentService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { formatDate, formatRelativeTime } from '../../utils/format';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import { formatDate, formatRelativeTime } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
|
||||
const DashboardPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Heart, AlertCircle, ArrowLeft } from 'lucide-react';
|
||||
import { RoomCard, RoomCardSkeleton } from
|
||||
'../../components/rooms';
|
||||
import { RoomCard, RoomCardSkeleton } from '../../features/rooms/components';
|
||||
import useFavoritesStore from
|
||||
'../../store/useFavoritesStore';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
|
||||
|
||||
const FavoritesPage: React.FC = () => {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
|
||||
@@ -7,16 +7,15 @@ import {
|
||||
ArrowLeft,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getBookingById, type Booking } from
|
||||
'../../services/api/bookingService';
|
||||
import { getBookingById, type Booking } from '../../features/bookings/services/bookingService';
|
||||
import {
|
||||
getPaymentsByBookingId,
|
||||
type Payment,
|
||||
} from '../../services/api/paymentService';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import StripePaymentWrapper from '../../components/payments/StripePaymentWrapper';
|
||||
} from '../../features/payments/services/paymentService';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
import StripePaymentWrapper from '../../features/payments/components/StripePaymentWrapper';
|
||||
|
||||
const FullPaymentPage: React.FC = () => {
|
||||
const { bookingId } = useParams<{ bookingId: string }>();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Users, Calendar, Building2, ArrowRight } from 'lucide-react';
|
||||
import { groupBookingService, GroupBooking } from '../../services/api';
|
||||
import groupBookingService, { GroupBooking } from '../../features/bookings/services/groupBookingService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import CreateGroupBookingModal from '../../components/shared/CreateGroupBookingModal';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import CreateGroupBookingModal from '../../features/hotel_services/components/CreateGroupBookingModal';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const GroupBookingPage: React.FC = () => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { ArrowLeft, Download, FileText, CheckCircle, Clock, XCircle } from 'lucide-react';
|
||||
import { invoiceService, Invoice } from '../../services/api';
|
||||
import invoiceService, { Invoice } from '../../features/payments/services/invoiceService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
|
||||
const InvoicePage: React.FC = () => {
|
||||
|
||||
@@ -12,15 +12,16 @@ import {
|
||||
CreditCard
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import loyaltyService, {
|
||||
UserLoyaltyStatus,
|
||||
PointsTransaction,
|
||||
LoyaltyReward,
|
||||
RewardRedemption,
|
||||
Referral
|
||||
} from '../../services/api/loyaltyService';
|
||||
import { formatDate } from '../../utils/format';
|
||||
} from '../../features/loyalty/services/loyaltyService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
|
||||
type Tab = 'overview' | 'rewards' | 'history' | 'referrals';
|
||||
|
||||
|
||||
@@ -19,14 +19,14 @@ import { toast } from 'react-toastify';
|
||||
import {
|
||||
getMyBookings,
|
||||
type Booking,
|
||||
} from '../../services/api/bookingService';
|
||||
import CancelBookingModal from '../../components/booking/CancelBookingModal';
|
||||
} from '../../features/bookings/services/bookingService';
|
||||
import CancelBookingModal from '../../features/bookings/components/CancelBookingModal';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import EmptyState from '../../components/common/EmptyState';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
|
||||
const MyBookingsPage: React.FC = () => {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { XCircle, ArrowLeft, Loader2 } from 'lucide-react';
|
||||
import { cancelPayPalPayment } from '../../services/api/paymentService';
|
||||
import { cancelPayPalPayment } from '../../features/payments/services/paymentService';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const PayPalCancelPage: React.FC = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { capturePayPalPayment, cancelPayPalPayment } from '../../services/api/paymentService';
|
||||
import { capturePayPalPayment, cancelPayPalPayment } from '../../features/payments/services/paymentService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
||||
|
||||
|
||||
@@ -19,15 +19,13 @@ import {
|
||||
getBookingById,
|
||||
generateQRCode,
|
||||
type Booking,
|
||||
} from '../../services/api/bookingService';
|
||||
import { confirmBankTransfer } from
|
||||
'../../services/api/paymentService';
|
||||
} from '../../features/bookings/services/bookingService';
|
||||
import { confirmBankTransfer } from '../../features/payments/services/paymentService';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import PaymentStatusBadge from
|
||||
'../../components/common/PaymentStatusBadge';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import PaymentStatusBadge from '../../shared/components/PaymentStatusBadge';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
|
||||
const PaymentConfirmationPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Receipt,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
||||
import { useCompanySettings } from '../../shared/contexts/CompanySettingsContext';
|
||||
|
||||
const PaymentResultPage: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -21,12 +21,13 @@ import {
|
||||
KeyRound
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import authService from '../../services/api/authService';
|
||||
import authService from '../../features/auth/services/authService';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import { useGlobalLoading } from '../../contexts/LoadingContext';
|
||||
import { normalizeImageUrl } from '../../utils/imageUtils';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import { useGlobalLoading } from '../../shared/contexts/LoadingContext';
|
||||
import { normalizeImageUrl } from '../../shared/utils/imageUtils';
|
||||
|
||||
const profileValidationSchema = yup.object().shape({
|
||||
name: yup
|
||||
@@ -200,7 +201,7 @@ const ProfilePage: React.FC = () => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { updateUser } = await import('../../services/api/userService');
|
||||
const { updateUser } = await import('../../features/auth/services/userService');
|
||||
const user = userInfo && 'user' in userInfo ? userInfo.user : userInfo;
|
||||
const response = await updateUser((user as any)?.id || (userInfo as any)?.id, {
|
||||
full_name: data.name,
|
||||
@@ -251,7 +252,7 @@ const ProfilePage: React.FC = () => {
|
||||
resetPassword();
|
||||
}
|
||||
} else {
|
||||
const { updateUser } = await import('../../services/api/userService');
|
||||
const { updateUser } = await import('../../features/auth/services/userService');
|
||||
const user = userInfo && 'user' in userInfo ? userInfo.user : userInfo;
|
||||
const response = await updateUser((user as any)?.id || (userInfo as any)?.id, {
|
||||
password: data.newPassword,
|
||||
|
||||
@@ -10,16 +10,15 @@ import {
|
||||
Award,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { getRoomByNumber, getRoomBookedDates, type Room } from
|
||||
'../../services/api/roomService';
|
||||
import RoomGallery from '../../components/rooms/RoomGallery';
|
||||
import RoomAmenities from '../../components/rooms/RoomAmenities';
|
||||
import ReviewSection from '../../components/rooms/ReviewSection';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { getRoomByNumber, getRoomBookedDates, type Room } from '../../features/rooms/services/roomService';
|
||||
import RoomGallery from '../../features/rooms/components/RoomGallery';
|
||||
import RoomAmenities from '../../features/rooms/components/RoomAmenities';
|
||||
import ReviewSection from '../../features/rooms/components/ReviewSection';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import LuxuryBookingModal from '../../components/booking/LuxuryBookingModal';
|
||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||
import LuxuryBookingModal from '../../features/bookings/components/LuxuryBookingModal';
|
||||
import { useAuthModal } from '../../features/auth/contexts/AuthModalContext';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const RoomDetailPage: React.FC = () => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useSearchParams, Link } from 'react-router-dom';
|
||||
import { getRooms } from '../../services/api/roomService';
|
||||
import type { Room } from '../../services/api/roomService';
|
||||
import RoomFilter from '../../components/rooms/RoomFilter';
|
||||
import RoomCard from '../../components/rooms/RoomCard';
|
||||
import RoomCardSkeleton from '../../components/rooms/RoomCardSkeleton';
|
||||
import Pagination from '../../components/rooms/Pagination';
|
||||
import { getRooms } from '../../features/rooms/services/roomService';
|
||||
import type { Room } from '../../features/rooms/services/roomService';
|
||||
import RoomFilter from '../../features/rooms/components/RoomFilter';
|
||||
import RoomCard from '../../features/rooms/components/RoomCard';
|
||||
import RoomCardSkeleton from '../../features/rooms/components/RoomCardSkeleton';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { ArrowLeft, Hotel, Filter, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
|
||||
const RoomListPage: React.FC = () => {
|
||||
|
||||
@@ -12,16 +12,13 @@ import {
|
||||
Home,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
RoomCard,
|
||||
RoomCardSkeleton,
|
||||
Pagination,
|
||||
} from '../../components/rooms';
|
||||
import { searchAvailableRooms } from
|
||||
'../../services/api/roomService';
|
||||
import type { Room } from '../../services/api/roomService';
|
||||
import RoomCard from '../../features/rooms/components/RoomCard';
|
||||
import RoomCardSkeleton from '../../features/rooms/components/RoomCardSkeleton';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { searchAvailableRooms } from '../../features/rooms/services/roomService';
|
||||
import type { Room } from '../../features/rooms/services/roomService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
|
||||
const SearchResultsPage: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -15,14 +15,14 @@ import {
|
||||
MapPin,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import advancedRoomService, {
|
||||
RoomStatusBoardItem,
|
||||
} from '../../services/api/advancedRoomService';
|
||||
import { roomService } from '../../services/api';
|
||||
import MaintenanceManagement from '../../components/shared/MaintenanceManagement';
|
||||
import HousekeepingManagement from '../../components/shared/HousekeepingManagement';
|
||||
import InspectionManagement from '../../components/shared/InspectionManagement';
|
||||
} from '../../features/rooms/services/advancedRoomService';
|
||||
import roomService from '../../features/rooms/services/roomService';
|
||||
import MaintenanceManagement from '../../features/hotel_services/components/MaintenanceManagement';
|
||||
import HousekeepingManagement from '../../features/hotel_services/components/HousekeepingManagement';
|
||||
import InspectionManagement from '../../features/hotel_services/components/InspectionManagement';
|
||||
|
||||
type Tab = 'status-board' | 'maintenance' | 'housekeeping' | 'inspections';
|
||||
|
||||
|
||||
@@ -29,14 +29,17 @@ import {
|
||||
Award
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState, ExportButton } from '../../components/common';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import { reportService, ReportData, reviewService, Review } from '../../services/api';
|
||||
import { auditService, AuditLog, AuditLogFilters } from '../../services/api/auditService';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import ExportButton from '../../shared/components/ExportButton';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import reportService, { ReportData } from '../../features/analytics/services/reportService';
|
||||
import reviewService, { Review } from '../../features/reviews/services/reviewService';
|
||||
import { auditService, AuditLog, AuditLogFilters } from '../../features/analytics/services/auditService';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import analyticsService, {
|
||||
ComprehensiveAnalyticsData,
|
||||
RevPARData,
|
||||
@@ -53,10 +56,10 @@ import analyticsService, {
|
||||
ProfitLossData,
|
||||
PaymentMethodAnalyticsData,
|
||||
RefundAnalysisData,
|
||||
} from '../../services/api/analyticsService';
|
||||
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../components/analytics/SimpleChart';
|
||||
import { exportData } from '../../utils/exportUtils';
|
||||
import CustomReportBuilder from '../../components/analytics/CustomReportBuilder';
|
||||
} from '../../features/analytics/services/analyticsService';
|
||||
import { SimpleBarChart, SimpleLineChart, SimplePieChart, KPICard } from '../../features/analytics/components/SimpleChart';
|
||||
import { exportData } from '../../shared/utils/exportUtils';
|
||||
import CustomReportBuilder from '../../features/analytics/components/CustomReportBuilder';
|
||||
|
||||
type AnalyticsTab = 'overview' | 'reports' | 'revenue' | 'operational' | 'guest' | 'financial' | 'audit-logs' | 'reviews';
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Search, Eye, XCircle, CheckCircle, Loader2, FileText, Plus } from 'lucide-react';
|
||||
import { bookingService, Booking, invoiceService } from '../../services/api';
|
||||
import bookingService, { Booking } from '../../features/bookings/services/bookingService';
|
||||
import invoiceService from '../../features/payments/services/invoiceService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import CreateBookingModal from '../../components/shared/CreateBookingModal';
|
||||
import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal';
|
||||
|
||||
const BookingManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { CheckCircle, XCircle, Send, Clock, User, X, RefreshCw } from 'lucide-react';
|
||||
import { chatService, type Chat, type ChatMessage } from '../../services/api';
|
||||
import chatService, { type Chat, type ChatMessage } from '../../features/notifications/services/chatService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import { useChatNotifications } from '../../contexts/ChatNotificationContext';
|
||||
import { useChatNotifications } from '../../features/notifications/contexts/ChatNotificationContext';
|
||||
|
||||
const ChatManagementPage: React.FC = () => {
|
||||
const [chats, setChats] = useState<Chat[]>([]);
|
||||
|
||||
@@ -6,14 +6,17 @@ import {
|
||||
RefreshCw,
|
||||
CreditCard
|
||||
} from 'lucide-react';
|
||||
import { reportService, ReportData, paymentService, bookingService } from '../../services/api';
|
||||
import type { Payment } from '../../services/api/paymentService';
|
||||
import type { Booking } from '../../services/api/bookingService';
|
||||
import reportService, { ReportData } from '../../features/analytics/services/reportService';
|
||||
import paymentService from '../../features/payments/services/paymentService';
|
||||
import bookingService from '../../features/bookings/services/bookingService';
|
||||
import type { Payment } from '../../features/payments/services/paymentService';
|
||||
import type { Booking } from '../../features/bookings/services/bookingService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState } from '../../components/common';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const StaffDashboardPage: React.FC = () => {
|
||||
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
Phone,
|
||||
MapPin,
|
||||
} from 'lucide-react';
|
||||
import { guestProfileService, GuestProfile, GuestListItem, GuestTag, GuestSegment, GuestSearchParams, GuestPreference } from '../../services/api';
|
||||
import guestProfileService, { GuestProfile, GuestListItem, GuestTag, GuestSegment, GuestSearchParams, GuestPreference } from '../../features/guest_management/services/guestProfileService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
|
||||
type TabType = 'list' | 'profile';
|
||||
|
||||
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
Save
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loading, EmptyState, ConfirmationDialog } from '../../components/common';
|
||||
import loyaltyService, { LoyaltyTier, LoyaltyReward } from '../../services/api/loyaltyService';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import ConfirmationDialog from '../../shared/components/ConfirmationDialog';
|
||||
import loyaltyService, { LoyaltyTier, LoyaltyReward } from '../../features/loyalty/services/loyaltyService';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
|
||||
type Tab = 'users' | 'tiers' | 'rewards';
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Search } from 'lucide-react';
|
||||
import { paymentService } from '../../services/api';
|
||||
import type { Payment } from '../../services/api/paymentService';
|
||||
import paymentService from '../../features/payments/services/paymentService';
|
||||
import type { Payment } from '../../features/payments/services/paymentService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import { ExportButton } from '../../components/common';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../utils/format';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import ExportButton from '../../shared/components/ExportButton';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
|
||||
const PaymentManagementPage: React.FC = () => {
|
||||
const { formatCurrency } = useFormatCurrency();
|
||||
|
||||
@@ -24,15 +24,17 @@ import {
|
||||
Calendar,
|
||||
Wrench
|
||||
} from 'lucide-react';
|
||||
import { bookingService, Booking, roomService, Room, serviceService, Service } from '../../services/api';
|
||||
import bookingService, { Booking } from '../../features/bookings/services/bookingService';
|
||||
import roomService, { Room } from '../../features/rooms/services/roomService';
|
||||
import serviceService, { Service } from '../../features/hotel_services/services/serviceService';
|
||||
import { toast } from 'react-toastify';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import CurrencyIcon from '../../components/common/CurrencyIcon';
|
||||
import Pagination from '../../components/common/Pagination';
|
||||
import apiClient from '../../services/api/apiClient';
|
||||
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../utils/format';
|
||||
import CreateBookingModal from '../../components/shared/CreateBookingModal';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import CurrencyIcon from '../../shared/components/CurrencyIcon';
|
||||
import Pagination from '../../shared/components/Pagination';
|
||||
import apiClient from '../../shared/services/apiClient';
|
||||
import { useFormatCurrency } from '../../features/payments/hooks/useFormatCurrency';
|
||||
import { parseDateLocal } from '../../shared/utils/format';
|
||||
import CreateBookingModal from '../../features/hotel_services/components/CreateBookingModal';
|
||||
|
||||
type ReceptionTab = 'overview' | 'check-in' | 'check-out' | 'bookings' | 'rooms' | 'services';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user