This commit is contained in:
Iliyan Angelov
2025-11-18 23:35:19 +02:00
parent ab832f851b
commit 2043ac897c
27 changed files with 2947 additions and 323 deletions

View File

@@ -14,8 +14,10 @@ import {
import { Link } from 'react-router-dom';
import { pageContentService } from '../services/api';
import type { PageContent } from '../services/api/pageContentService';
import { useCompanySettings } from '../contexts/CompanySettingsContext';
const AboutPage: React.FC = () => {
const { settings } = useCompanySettings();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
useEffect(() => {
@@ -48,6 +50,11 @@ const AboutPage: React.FC = () => {
fetchPageContent();
}, []);
// Get phone, email, and address from centralized company settings
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';
// Default values
const defaultValues = [
{
@@ -253,11 +260,11 @@ const AboutPage: React.FC = () => {
Address
</h3>
<p className="text-gray-600">
{(pageContent?.contact_info?.address || '123 Luxury Street\nCity, State 12345\nCountry')
{displayAddress
.split('\n').map((line, i) => (
<React.Fragment key={i}>
{line}
{i < 2 && <br />}
{i < displayAddress.split('\n').length - 1 && <br />}
</React.Fragment>
))}
</p>
@@ -270,8 +277,8 @@ const AboutPage: React.FC = () => {
Phone
</h3>
<p className="text-gray-600">
<a href={`tel:${pageContent?.contact_info?.phone || '+1234567890'}`} className="hover:text-[#d4af37] transition-colors">
{pageContent?.contact_info?.phone || '+1 (234) 567-890'}
<a href={`tel:${displayPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`} className="hover:text-[#d4af37] transition-colors">
{displayPhone}
</a>
</p>
</div>
@@ -283,8 +290,8 @@ const AboutPage: React.FC = () => {
Email
</h3>
<p className="text-gray-600">
<a href={`mailto:${pageContent?.contact_info?.email || 'info@luxuryhotel.com'}`} className="hover:text-[#d4af37] transition-colors">
{pageContent?.contact_info?.email || 'info@luxuryhotel.com'}
<a href={`mailto:${displayEmail}`} className="hover:text-[#d4af37] transition-colors">
{displayEmail}
</a>
</p>
</div>

View File

@@ -3,9 +3,11 @@ 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';
const ContactPage: React.FC = () => {
const { settings } = useCompanySettings();
const [pageContent, setPageContent] = useState<PageContent | null>(null);
const [formData, setFormData] = useState({
name: '',
@@ -103,6 +105,11 @@ const ContactPage: React.FC = () => {
fetchPageContent();
}, []);
// Get phone, email, and address from centralized company settings
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 }));
@@ -181,9 +188,9 @@ const ContactPage: React.FC = () => {
</div>
<div>
<h3 className="text-sm sm:text-base font-medium text-[#d4af37] mb-1 sm:mb-2 tracking-wide">Email</h3>
<p className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed">
{pageContent?.contact_info?.email || "We'll respond within 24 hours"}
</p>
<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>
@@ -193,9 +200,9 @@ const ContactPage: React.FC = () => {
</div>
<div>
<h3 className="text-sm sm:text-base font-medium text-[#d4af37] mb-1 sm:mb-2 tracking-wide">Phone</h3>
<p className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed">
{pageContent?.contact_info?.phone || 'Available 24/7 for your convenience'}
</p>
<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>
@@ -205,8 +212,8 @@ const ContactPage: React.FC = () => {
</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">
{pageContent?.contact_info?.address || 'Visit us at our hotel reception'}
<p className="text-gray-300 font-light text-xs sm:text-sm leading-relaxed whitespace-pre-line">
{displayAddress}
</p>
</div>
</div>
@@ -236,8 +243,7 @@ const ContactPage: React.FC = () => {
<div className="mt-5 sm:mt-6 md:mt-7 pt-5 sm:pt-6 md:pt-7 border-t border-[#d4af37]/30">
<p className="text-gray-400 font-light text-xs sm:text-sm leading-relaxed tracking-wide">
Our team is here to help you with any questions about your stay,
bookings, or special requests. We're committed to exceeding your expectations.
{pageContent?.content || "Our team is here to help you with any questions about your stay, bookings, or special requests. We're committed to exceeding your expectations."}
</p>
</div>
</div>

View File

@@ -23,7 +23,9 @@ import {
Upload,
Loader2,
Check,
XCircle
XCircle,
Award,
Shield
} from 'lucide-react';
import { pageContentService, PageContent, PageType, UpdatePageContentData, bannerService, Banner } from '../../services/api';
import { toast } from 'react-toastify';
@@ -138,7 +140,6 @@ const PageContentDashboard: React.FC = () => {
subtitle: contents.contact.subtitle || '',
description: contents.contact.description || '',
content: contents.contact.content || '',
contact_info: contents.contact.contact_info || { phone: '', email: '', address: '' },
map_url: contents.contact.map_url || '',
meta_title: contents.contact.meta_title || '',
meta_description: contents.contact.meta_description || '',
@@ -165,9 +166,9 @@ const PageContentDashboard: React.FC = () => {
setFooterData({
title: contents.footer.title || '',
description: contents.footer.description || '',
contact_info: contents.footer.contact_info || { phone: '', email: '', address: '' },
social_links: contents.footer.social_links || {},
footer_links: contents.footer.footer_links || { quick_links: [], support_links: [] },
badges: contents.footer.badges || [],
meta_title: contents.footer.meta_title || '',
meta_description: contents.footer.meta_description || '',
});
@@ -190,7 +191,13 @@ const PageContentDashboard: React.FC = () => {
const handleSave = async (pageType: PageType, data: UpdatePageContentData) => {
try {
setSaving(true);
await pageContentService.updatePageContent(pageType, data);
// Remove contact_info for contact and footer pages since it's now managed centrally
const { contact_info, ...dataToSave } = data;
if (pageType === 'contact' || pageType === 'footer') {
await pageContentService.updatePageContent(pageType, dataToSave);
} else {
await pageContentService.updatePageContent(pageType, data);
}
toast.success(`${pageType.charAt(0).toUpperCase() + pageType.slice(1)} content saved successfully`);
await fetchAllPageContents();
} catch (error: any) {
@@ -993,44 +1000,11 @@ const PageContentDashboard: React.FC = () => {
</div>
<div className="border-t border-gray-200 pt-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">Contact Information</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Phone</label>
<input
type="tel"
value={contactData.contact_info?.phone || ''}
onChange={(e) => setContactData({
...contactData,
contact_info: { ...contactData.contact_info, phone: e.target.value }
})}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Email</label>
<input
type="email"
value={contactData.contact_info?.email || ''}
onChange={(e) => setContactData({
...contactData,
contact_info: { ...contactData.contact_info, email: e.target.value }
})}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Address</label>
<input
type="text"
value={contactData.contact_info?.address || ''}
onChange={(e) => setContactData({
...contactData,
contact_info: { ...contactData.contact_info, address: e.target.value }
})}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
/>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<p className="text-sm text-blue-800">
<strong>Note:</strong> Contact information (phone, email, address) is now managed centrally in <strong>Settings Company Info</strong>.
These fields will be displayed across the entire application.
</p>
</div>
</div>
@@ -1065,6 +1039,23 @@ const PageContentDashboard: React.FC = () => {
</div>
</div>
<div className="border-t border-gray-200 pt-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">Help Message</h3>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Additional Information Text</label>
<textarea
value={contactData.content || ''}
onChange={(e) => setContactData({ ...contactData, content: e.target.value })}
rows={3}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
placeholder="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 className="text-xs text-gray-500 mt-2">
This text will appear below the contact information and map on the contact page.
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
@@ -1213,44 +1204,11 @@ const PageContentDashboard: React.FC = () => {
</div>
<div className="border-t border-gray-200 pt-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">Contact Information</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Phone</label>
<input
type="tel"
value={footerData.contact_info?.phone || ''}
onChange={(e) => setFooterData({
...footerData,
contact_info: { ...footerData.contact_info, phone: e.target.value }
})}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Email</label>
<input
type="email"
value={footerData.contact_info?.email || ''}
onChange={(e) => setFooterData({
...footerData,
contact_info: { ...footerData.contact_info, email: e.target.value }
})}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">Address</label>
<input
type="text"
value={footerData.contact_info?.address || ''}
onChange={(e) => setFooterData({
...footerData,
contact_info: { ...footerData.contact_info, address: e.target.value }
})}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
/>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<p className="text-sm text-blue-800">
<strong>Note:</strong> Contact information (phone, email, address) is now managed centrally in <strong>Settings Company Info</strong>.
These fields will be displayed across the entire application, including the footer.
</p>
</div>
</div>
@@ -1325,6 +1283,123 @@ const PageContentDashboard: React.FC = () => {
</div>
</div>
<div className="border-t border-gray-200 pt-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">Footer Badges</h3>
<p className="text-sm text-gray-600 mb-4">Customize the badges displayed in the footer (e.g., "5-Star Rated", "Award Winning").</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Badge 1 */}
<div className="space-y-4 p-6 bg-gray-50 rounded-xl border border-gray-200">
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
<Award className="w-4 h-4 text-gray-600" />
Badge 1
</label>
<input
type="text"
value={footerData.badges?.[0]?.text || ''}
onChange={(e) => {
const badges = footerData.badges || [];
const updated = [...badges];
if (updated[0]) {
updated[0] = { ...updated[0], text: e.target.value };
} else {
updated[0] = { text: e.target.value, icon: 'Award' };
}
setFooterData({ ...footerData, badges: updated });
}}
placeholder="5-Star Rated"
className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 transition-all duration-200 text-sm"
/>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-2">Icon</label>
<select
value={footerData.badges?.[0]?.icon || 'Award'}
onChange={(e) => {
const badges = footerData.badges || [];
const updated = [...badges];
if (updated[0]) {
updated[0] = { ...updated[0], icon: e.target.value };
} else {
updated[0] = { text: '', icon: e.target.value };
}
setFooterData({ ...footerData, badges: updated });
}}
className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 transition-all duration-200 text-sm"
>
<option value="Award">Award</option>
<option value="Star">Star</option>
<option value="Trophy">Trophy</option>
<option value="Medal">Medal</option>
<option value="BadgeCheck">Badge Check</option>
<option value="CheckCircle">Check Circle</option>
<option value="Shield">Shield</option>
<option value="Heart">Heart</option>
<option value="Crown">Crown</option>
<option value="Gem">Gem</option>
<option value="Zap">Zap</option>
<option value="Target">Target</option>
<option value="TrendingUp">Trending Up</option>
</select>
</div>
</div>
{/* Badge 2 */}
<div className="space-y-4 p-6 bg-gray-50 rounded-xl border border-gray-200">
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
<Shield className="w-4 h-4 text-gray-600" />
Badge 2
</label>
<input
type="text"
value={footerData.badges?.[1]?.text || ''}
onChange={(e) => {
const badges = footerData.badges || [];
const updated = [...badges];
if (updated[1]) {
updated[1] = { ...updated[1], text: e.target.value };
} else {
updated[1] = { text: e.target.value, icon: 'Shield' };
}
setFooterData({ ...footerData, badges: updated });
}}
placeholder="Award Winning"
className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 transition-all duration-200 text-sm"
/>
<div>
<label className="block text-xs font-semibold text-gray-700 mb-2">Icon</label>
<select
value={footerData.badges?.[1]?.icon || 'Shield'}
onChange={(e) => {
const badges = footerData.badges || [];
const updated = [...badges];
if (updated[1]) {
updated[1] = { ...updated[1], icon: e.target.value };
} else {
updated[1] = { text: '', icon: e.target.value };
}
setFooterData({ ...footerData, badges: updated });
}}
className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 transition-all duration-200 text-sm"
>
<option value="Award">Award</option>
<option value="Star">Star</option>
<option value="Trophy">Trophy</option>
<option value="Medal">Medal</option>
<option value="BadgeCheck">Badge Check</option>
<option value="CheckCircle">Check Circle</option>
<option value="Shield">Shield</option>
<option value="Heart">Heart</option>
<option value="Crown">Crown</option>
<option value="Gem">Gem</option>
<option value="Zap">Zap</option>
<option value="Target">Target</option>
<option value="TrendingUp">Trending Up</option>
</select>
</div>
</div>
</div>
</div>
<div className="flex justify-end pt-4 border-t border-gray-200">
<button
onClick={() => handleSave('footer', footerData)}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { Link } from 'react-router-dom';
@@ -15,14 +15,20 @@ import {
forgotPasswordSchema,
ForgotPasswordFormData,
} from '../../utils/validationSchemas';
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
const ForgotPasswordPage: React.FC = () => {
const { forgotPassword, isLoading, error, clearError } =
useAuthStore();
const { settings } = useCompanySettings();
const [isSuccess, setIsSuccess] = useState(false);
const [submittedEmail, setSubmittedEmail] = useState('');
// Get email and phone from centralized company settings
const supportEmail = settings.company_email || 'support@hotel.com';
const supportPhone = settings.company_phone || '1900-xxxx';
// React Hook Form setup
const {
register,
@@ -306,18 +312,22 @@ const ForgotPasswordPage: React.FC = () => {
If you're having trouble resetting your password,
please contact our support team via email{' '}
<a
href="mailto:support@hotel.com"
href={`mailto:${supportEmail}`}
className="text-blue-600 hover:underline"
>
support@hotel.com
</a>{' '}
or hotline{' '}
<a
href="tel:1900-xxxx"
className="text-blue-600 hover:underline"
>
1900-xxxx
{supportEmail}
</a>
{supportPhone && (
<>
{' '}or hotline{' '}
<a
href={`tel:${supportPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`}
className="text-blue-600 hover:underline"
>
{supportPhone}
</a>
</>
)}
</p>
</div>
</div>

View File

@@ -8,11 +8,17 @@ import {
Receipt,
Loader2,
} from 'lucide-react';
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
const PaymentResultPage: React.FC = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [countdown, setCountdown] = useState(10);
const { settings } = useCompanySettings();
// Get email and phone from centralized company settings
const supportEmail = settings.company_email || 'support@hotel.com';
const supportPhone = settings.company_phone || '1900 xxxx';
const status = searchParams.get('status');
const bookingId = searchParams.get('bookingId');
@@ -229,18 +235,22 @@ const PaymentResultPage: React.FC = () => {
<p>
If you have any issues, please contact{' '}
<a
href="mailto:support@hotel.com"
href={`mailto:${supportEmail}`}
className="text-indigo-600 hover:underline"
>
support@hotel.com
</a>{' '}
or call{' '}
<a
href="tel:1900xxxx"
className="text-indigo-600 hover:underline"
>
1900 xxxx
{supportEmail}
</a>
{supportPhone && (
<>
{' '}or call{' '}
<a
href={`tel:${supportPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`}
className="text-indigo-600 hover:underline"
>
{supportPhone}
</a>
</>
)}
</p>
</div>
</div>