Files
Hotel-Booking/Frontend/src/pages/ContactPage.tsx
Iliyan Angelov cf97df9aeb updates
2025-11-28 20:24:58 +02:00

519 lines
26 KiB
TypeScript

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;