updates
This commit is contained in:
@@ -10,6 +10,7 @@ import 'react-toastify/dist/ReactToastify.css';
|
||||
import { GlobalLoadingProvider } from './contexts/GlobalLoadingContext';
|
||||
import { CookieConsentProvider } from './contexts/CookieConsentContext';
|
||||
import { CurrencyProvider } from './contexts/CurrencyContext';
|
||||
import { CompanySettingsProvider } from './contexts/CompanySettingsContext';
|
||||
import OfflineIndicator from './components/common/OfflineIndicator';
|
||||
import CookieConsentBanner from './components/common/CookieConsentBanner';
|
||||
import AnalyticsLoader from './components/common/AnalyticsLoader';
|
||||
@@ -121,6 +122,7 @@ function App() {
|
||||
<GlobalLoadingProvider>
|
||||
<CookieConsentProvider>
|
||||
<CurrencyProvider>
|
||||
<CompanySettingsProvider>
|
||||
<BrowserRouter
|
||||
future={{
|
||||
v7_startTransition: true,
|
||||
@@ -337,6 +339,7 @@ function App() {
|
||||
<AnalyticsLoader />
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
</CompanySettingsProvider>
|
||||
</CurrencyProvider>
|
||||
</CookieConsentProvider>
|
||||
</GlobalLoadingProvider>
|
||||
|
||||
@@ -12,13 +12,26 @@ import {
|
||||
Youtube,
|
||||
Award,
|
||||
Shield,
|
||||
Star
|
||||
Star,
|
||||
Trophy,
|
||||
Medal,
|
||||
BadgeCheck,
|
||||
CheckCircle,
|
||||
Heart,
|
||||
Crown,
|
||||
Gem,
|
||||
Zap,
|
||||
Target,
|
||||
TrendingUp,
|
||||
LucideIcon
|
||||
} from 'lucide-react';
|
||||
import CookiePreferencesLink from '../common/CookiePreferencesLink';
|
||||
import { pageContentService } from '../../services/api';
|
||||
import type { PageContent } from '../../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -37,6 +50,38 @@ const Footer: React.FC = () => {
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
// Get phone, email, and address from centralized company settings
|
||||
const displayPhone = settings.company_phone || null;
|
||||
const displayEmail = settings.company_email || null;
|
||||
const displayAddress = settings.company_address || '123 ABC Street, District 1\nHo Chi Minh City, Vietnam';
|
||||
|
||||
// Get logo URL from centralized company settings
|
||||
const logoUrl = settings.company_logo_url
|
||||
? (settings.company_logo_url.startsWith('http')
|
||||
? settings.company_logo_url
|
||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`)
|
||||
: null;
|
||||
|
||||
// Icon map for badges
|
||||
const iconMap: Record<string, LucideIcon> = {
|
||||
Award,
|
||||
Star,
|
||||
Trophy,
|
||||
Medal,
|
||||
BadgeCheck,
|
||||
CheckCircle,
|
||||
Shield,
|
||||
Heart,
|
||||
Crown,
|
||||
Gem,
|
||||
Zap,
|
||||
Target,
|
||||
TrendingUp,
|
||||
};
|
||||
|
||||
// Get badges from page content
|
||||
const badges = pageContent?.badges || [];
|
||||
|
||||
// Default links
|
||||
const defaultQuickLinks = [
|
||||
{ label: 'Home', url: '/' },
|
||||
@@ -76,16 +121,26 @@ const Footer: React.FC = () => {
|
||||
{/* Company Info - Enhanced */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="flex items-center space-x-3 mb-6">
|
||||
<div className="relative">
|
||||
<Hotel className="w-10 h-10 text-[#d4af37]" />
|
||||
<div className="absolute inset-0 bg-[#d4af37]/20 blur-xl"></div>
|
||||
</div>
|
||||
{logoUrl ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={settings.company_name}
|
||||
className="h-10 w-auto object-contain drop-shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Hotel className="w-10 h-10 text-[#d4af37]" />
|
||||
<div className="absolute inset-0 bg-[#d4af37]/20 blur-xl"></div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="text-2xl font-serif font-semibold text-white tracking-wide">
|
||||
{pageContent?.title || 'Luxury Hotel'}
|
||||
{settings.company_name || pageContent?.title || 'Luxury Hotel'}
|
||||
</span>
|
||||
<p className="text-xs text-[#d4af37]/80 font-light tracking-widest uppercase mt-0.5">
|
||||
Excellence Redefined
|
||||
{settings.company_tagline || 'Excellence Redefined'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,16 +149,20 @@ const Footer: React.FC = () => {
|
||||
</p>
|
||||
|
||||
{/* Premium Certifications */}
|
||||
<div className="flex items-center space-x-6 mb-8">
|
||||
<div className="flex items-center space-x-2 text-[#d4af37]/90">
|
||||
<Award className="w-5 h-5" />
|
||||
<span className="text-xs font-medium tracking-wide">5-Star Rated</span>
|
||||
{badges.length > 0 && badges.some(b => b.text) && (
|
||||
<div className="flex items-center space-x-6 mb-8">
|
||||
{badges.map((badge, index) => {
|
||||
if (!badge.text) return null;
|
||||
const BadgeIcon = iconMap[badge.icon] || Award;
|
||||
return (
|
||||
<div key={index} className="flex items-center space-x-2 text-[#d4af37]/90">
|
||||
<BadgeIcon className="w-5 h-5" />
|
||||
<span className="text-xs font-medium tracking-wide">{badge.text}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-[#d4af37]/90">
|
||||
<Shield className="w-5 h-5" />
|
||||
<span className="text-xs font-medium tracking-wide">Award Winning</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Social Media - Premium Style */}
|
||||
<div className="flex items-center space-x-3">
|
||||
@@ -225,37 +284,37 @@ const Footer: React.FC = () => {
|
||||
<div className="absolute inset-0 bg-[#d4af37]/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-400 group-hover:text-gray-300 transition-colors leading-relaxed font-light">
|
||||
{((pageContent?.contact_info?.address || '123 ABC Street, District 1\nHo Chi Minh City, Vietnam')
|
||||
{(displayAddress
|
||||
.split('\n').map((line, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{line}
|
||||
{i < 1 && <br />}
|
||||
{i < displayAddress.split('\n').length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
)))}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center space-x-4 group">
|
||||
<div className="relative">
|
||||
<Phone className="w-5 h-5 text-[#d4af37]/80 group-hover:text-[#d4af37] transition-colors flex-shrink-0" />
|
||||
<div className="absolute inset-0 bg-[#d4af37]/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</div>
|
||||
{pageContent?.contact_info?.phone && (
|
||||
<a href={`tel:${pageContent.contact_info.phone}`} className="text-sm text-gray-400 group-hover:text-[#d4af37] transition-colors font-light tracking-wide">
|
||||
{pageContent.contact_info.phone}
|
||||
{displayPhone && (
|
||||
<li className="flex items-center space-x-4 group">
|
||||
<div className="relative">
|
||||
<Phone className="w-5 h-5 text-[#d4af37]/80 group-hover:text-[#d4af37] transition-colors flex-shrink-0" />
|
||||
<div className="absolute inset-0 bg-[#d4af37]/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</div>
|
||||
<a href={`tel:${displayPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`} className="text-sm text-gray-400 group-hover:text-[#d4af37] transition-colors font-light tracking-wide">
|
||||
{displayPhone}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
<li className="flex items-center space-x-4 group">
|
||||
<div className="relative">
|
||||
<Mail className="w-5 h-5 text-[#d4af37]/80 group-hover:text-[#d4af37] transition-colors flex-shrink-0" />
|
||||
<div className="absolute inset-0 bg-[#d4af37]/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</div>
|
||||
{pageContent?.contact_info?.email && (
|
||||
<a href={`mailto:${pageContent.contact_info.email}`} className="text-sm text-gray-400 group-hover:text-[#d4af37] transition-colors font-light tracking-wide">
|
||||
{pageContent.contact_info.email}
|
||||
</li>
|
||||
)}
|
||||
{displayEmail && (
|
||||
<li className="flex items-center space-x-4 group">
|
||||
<div className="relative">
|
||||
<Mail className="w-5 h-5 text-[#d4af37]/80 group-hover:text-[#d4af37] transition-colors flex-shrink-0" />
|
||||
<div className="absolute inset-0 bg-[#d4af37]/20 blur-md opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</div>
|
||||
<a href={`mailto:${displayEmail}`} className="text-sm text-gray-400 group-hover:text-[#d4af37] transition-colors font-light tracking-wide">
|
||||
{displayEmail}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{/* Star Rating Display */}
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
Phone,
|
||||
Mail,
|
||||
Calendar,
|
||||
MessageSquare,
|
||||
} from 'lucide-react';
|
||||
import { useClickOutside } from '../../hooks/useClickOutside';
|
||||
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
||||
|
||||
interface HeaderProps {
|
||||
isAuthenticated?: boolean;
|
||||
@@ -32,6 +32,16 @@ const Header: React.FC<HeaderProps> = ({
|
||||
userInfo = null,
|
||||
onLogout
|
||||
}) => {
|
||||
const { settings } = useCompanySettings();
|
||||
|
||||
// Get phone and email from centralized company settings
|
||||
const displayPhone = settings.company_phone || '+1 (234) 567-890';
|
||||
const displayEmail = settings.company_email || 'info@luxuryhotel.com';
|
||||
const logoUrl = settings.company_logo_url
|
||||
? (settings.company_logo_url.startsWith('http')
|
||||
? settings.company_logo_url
|
||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`)
|
||||
: null;
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] =
|
||||
useState(false);
|
||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||
@@ -66,14 +76,18 @@ const Header: React.FC<HeaderProps> = ({
|
||||
<div className="hidden lg:block bg-[#0a0a0a]/50 border-b border-[#d4af37]/10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
|
||||
<div className="flex items-center justify-end space-x-6 text-sm">
|
||||
<a href="tel:+1234567890" className="flex items-center space-x-2 text-[#d4af37] hover:text-[#f5d76e] transition-colors duration-300 font-light">
|
||||
<Phone className="w-3.5 h-3.5" />
|
||||
<span className="tracking-wide">+1 (234) 567-890</span>
|
||||
</a>
|
||||
<a href="mailto:info@luxuryhotel.com" className="flex items-center space-x-2 text-[#d4af37] hover:text-[#f5d76e] transition-colors duration-300 font-light">
|
||||
<Mail className="w-3.5 h-3.5" />
|
||||
<span className="tracking-wide">info@luxuryhotel.com</span>
|
||||
</a>
|
||||
{displayPhone && (
|
||||
<a href={`tel:${displayPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`} className="flex items-center space-x-2 text-[#d4af37] hover:text-[#f5d76e] transition-colors duration-300 font-light">
|
||||
<Phone className="w-3.5 h-3.5" />
|
||||
<span className="tracking-wide">{displayPhone}</span>
|
||||
</a>
|
||||
)}
|
||||
{displayEmail && (
|
||||
<a href={`mailto:${displayEmail}`} className="flex items-center space-x-2 text-[#d4af37] hover:text-[#f5d76e] transition-colors duration-300 font-light">
|
||||
<Mail className="w-3.5 h-3.5" />
|
||||
<span className="tracking-wide">{displayEmail}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,16 +101,26 @@ const Header: React.FC<HeaderProps> = ({
|
||||
className="flex items-center space-x-3
|
||||
group transition-all duration-300 hover:opacity-90"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37] to-[#f5d76e] rounded-full blur-md opacity-30 group-hover:opacity-50 transition-opacity duration-300"></div>
|
||||
<Hotel className="relative w-10 h-10 text-[#d4af37] drop-shadow-lg" />
|
||||
</div>
|
||||
{logoUrl ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={settings.company_name}
|
||||
className="h-10 w-auto object-contain drop-shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#d4af37] to-[#f5d76e] rounded-full blur-md opacity-30 group-hover:opacity-50 transition-opacity duration-300"></div>
|
||||
<Hotel className="relative w-10 h-10 text-[#d4af37] drop-shadow-lg" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-2xl font-serif font-semibold text-white tracking-tight leading-tight">
|
||||
Luxury Hotel
|
||||
{settings.company_name}
|
||||
</span>
|
||||
<span className="text-[10px] text-[#d4af37] tracking-[0.2em] uppercase font-light">
|
||||
Excellence Redefined
|
||||
{settings.company_tagline || 'Excellence Redefined'}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -710,7 +710,7 @@ const RoomAmenities: React.FC<RoomAmenitiesProps> = ({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2
|
||||
lg:grid-cols-3 gap-4"
|
||||
>
|
||||
{safeAmenities.slice(0, 10).map((amenity, index) => (
|
||||
{safeAmenities.map((amenity, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-3 p-3
|
||||
|
||||
@@ -97,7 +97,7 @@ const RoomFilter: React.FC<RoomFilterProps> = ({ onFilterChange }) => {
|
||||
import('../../services/api/roomService').then((mod) => {
|
||||
mod.getAmenities().then((res) => {
|
||||
const list = res.data?.amenities || [];
|
||||
if (mounted) setAvailableAmenities(list.slice(0, 8));
|
||||
if (mounted) setAvailableAmenities(list);
|
||||
}).catch(() => {});
|
||||
});
|
||||
return () => {
|
||||
|
||||
133
Frontend/src/contexts/CompanySettingsContext.tsx
Normal file
133
Frontend/src/contexts/CompanySettingsContext.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import systemSettingsService from '../services/api/systemSettingsService';
|
||||
|
||||
type CompanySettings = {
|
||||
company_name: string;
|
||||
company_tagline: string;
|
||||
company_logo_url: string;
|
||||
company_favicon_url: string;
|
||||
company_phone: string;
|
||||
company_email: string;
|
||||
company_address: string;
|
||||
};
|
||||
|
||||
type CompanySettingsContextValue = {
|
||||
settings: CompanySettings;
|
||||
isLoading: boolean;
|
||||
refreshSettings: () => Promise<void>;
|
||||
};
|
||||
|
||||
const defaultSettings: CompanySettings = {
|
||||
company_name: 'Luxury Hotel',
|
||||
company_tagline: 'Excellence Redefined',
|
||||
company_logo_url: '',
|
||||
company_favicon_url: '',
|
||||
company_phone: '',
|
||||
company_email: '',
|
||||
company_address: '',
|
||||
};
|
||||
|
||||
const CompanySettingsContext = createContext<CompanySettingsContextValue | undefined>(undefined);
|
||||
|
||||
export const useCompanySettings = () => {
|
||||
const context = useContext(CompanySettingsContext);
|
||||
if (!context) {
|
||||
throw new Error('useCompanySettings must be used within CompanySettingsProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface CompanySettingsProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const CompanySettingsProvider: React.FC<CompanySettingsProviderProps> = ({ children }) => {
|
||||
const [settings, setSettings] = useState<CompanySettings>(defaultSettings);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
// Load company settings from system settings
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await systemSettingsService.getCompanySettings();
|
||||
if (response.data) {
|
||||
setSettings({
|
||||
company_name: response.data.company_name || defaultSettings.company_name,
|
||||
company_tagline: response.data.company_tagline || defaultSettings.company_tagline,
|
||||
company_logo_url: response.data.company_logo_url || defaultSettings.company_logo_url,
|
||||
company_favicon_url: response.data.company_favicon_url || defaultSettings.company_favicon_url,
|
||||
company_phone: response.data.company_phone || defaultSettings.company_phone,
|
||||
company_email: response.data.company_email || defaultSettings.company_email,
|
||||
company_address: response.data.company_address || defaultSettings.company_address,
|
||||
});
|
||||
|
||||
// Update favicon if available
|
||||
if (response.data.company_favicon_url) {
|
||||
const faviconUrl = response.data.company_favicon_url.startsWith('http')
|
||||
? response.data.company_favicon_url
|
||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${response.data.company_favicon_url}`;
|
||||
|
||||
// Remove existing favicon links
|
||||
const existingLinks = document.querySelectorAll("link[rel~='icon']");
|
||||
existingLinks.forEach(link => link.remove());
|
||||
|
||||
// Add new favicon
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'icon';
|
||||
link.href = faviconUrl;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
// Update page title if company name is set
|
||||
if (response.data.company_name) {
|
||||
document.title = response.data.company_name;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading company settings:', error);
|
||||
// Keep default settings
|
||||
setSettings(defaultSettings);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
|
||||
// Listen for refresh events from settings page
|
||||
const handleRefresh = () => {
|
||||
loadSettings();
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('refreshCompanySettings', handleRefresh);
|
||||
return () => {
|
||||
window.removeEventListener('refreshCompanySettings', handleRefresh);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshSettings = async () => {
|
||||
await loadSettings();
|
||||
};
|
||||
|
||||
return (
|
||||
<CompanySettingsContext.Provider
|
||||
value={{
|
||||
settings,
|
||||
isLoading,
|
||||
refreshSettings,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CompanySettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface PageContent {
|
||||
quick_links?: Array<{ label: string; url: string }>;
|
||||
support_links?: Array<{ label: string; url: string }>;
|
||||
};
|
||||
badges?: Array<{ text: string; icon: string }>;
|
||||
hero_title?: string;
|
||||
hero_subtitle?: string;
|
||||
hero_image?: string;
|
||||
@@ -82,6 +83,7 @@ export interface UpdatePageContentData {
|
||||
quick_links?: Array<{ label: string; url: string }>;
|
||||
support_links?: Array<{ label: string; url: string }>;
|
||||
};
|
||||
badges?: Array<{ text: string; icon: string }>;
|
||||
hero_title?: string;
|
||||
hero_subtitle?: string;
|
||||
hero_image?: string;
|
||||
@@ -148,6 +150,11 @@ const pageContentService = {
|
||||
};
|
||||
}
|
||||
|
||||
// Handle badges array
|
||||
if (data.badges) {
|
||||
updateData.badges = data.badges; // Send as array, backend will convert to JSON
|
||||
}
|
||||
|
||||
// Handle values and features arrays
|
||||
if (data.values) {
|
||||
updateData.values = data.values; // Send as array, backend will convert to JSON
|
||||
|
||||
@@ -36,6 +36,91 @@ export interface UpdateStripeSettingsRequest {
|
||||
stripe_webhook_secret?: string;
|
||||
}
|
||||
|
||||
export interface SmtpSettingsResponse {
|
||||
status: string;
|
||||
data: {
|
||||
smtp_host: string;
|
||||
smtp_port: string;
|
||||
smtp_user: string;
|
||||
smtp_password: string;
|
||||
smtp_password_masked: string;
|
||||
smtp_from_email: string;
|
||||
smtp_from_name: string;
|
||||
smtp_use_tls: boolean;
|
||||
has_host: boolean;
|
||||
has_user: boolean;
|
||||
has_password: boolean;
|
||||
updated_at?: string | null;
|
||||
updated_by?: string | null;
|
||||
};
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface UpdateSmtpSettingsRequest {
|
||||
smtp_host?: string;
|
||||
smtp_port?: string;
|
||||
smtp_user?: string;
|
||||
smtp_password?: string;
|
||||
smtp_from_email?: string;
|
||||
smtp_from_name?: string;
|
||||
smtp_use_tls?: boolean;
|
||||
}
|
||||
|
||||
export interface TestSmtpEmailRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface TestSmtpEmailResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
recipient: string;
|
||||
sent_at: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CompanySettingsResponse {
|
||||
status: string;
|
||||
data: {
|
||||
company_name: string;
|
||||
company_tagline: string;
|
||||
company_logo_url: string;
|
||||
company_favicon_url: string;
|
||||
company_phone: string;
|
||||
company_email: string;
|
||||
company_address: string;
|
||||
updated_at?: string | null;
|
||||
updated_by?: string | null;
|
||||
};
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface UpdateCompanySettingsRequest {
|
||||
company_name?: string;
|
||||
company_tagline?: string;
|
||||
company_phone?: string;
|
||||
company_email?: string;
|
||||
company_address?: string;
|
||||
}
|
||||
|
||||
export interface UploadLogoResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
logo_url: string;
|
||||
full_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UploadFaviconResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
favicon_url: string;
|
||||
full_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
const systemSettingsService = {
|
||||
/**
|
||||
* Get platform currency (public endpoint)
|
||||
@@ -82,7 +167,123 @@ const systemSettingsService = {
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get SMTP settings (Admin only)
|
||||
*/
|
||||
getSmtpSettings: async (): Promise<SmtpSettingsResponse> => {
|
||||
const response = await apiClient.get<SmtpSettingsResponse>(
|
||||
'/api/admin/system-settings/smtp'
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update SMTP settings (Admin only)
|
||||
*/
|
||||
updateSmtpSettings: async (
|
||||
settings: UpdateSmtpSettingsRequest
|
||||
): Promise<SmtpSettingsResponse> => {
|
||||
const response = await apiClient.put<SmtpSettingsResponse>(
|
||||
'/api/admin/system-settings/smtp',
|
||||
settings
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Test SMTP email (Admin only)
|
||||
*/
|
||||
testSmtpEmail: async (
|
||||
email: string
|
||||
): Promise<TestSmtpEmailResponse> => {
|
||||
const response = await apiClient.post<TestSmtpEmailResponse>(
|
||||
'/api/admin/system-settings/smtp/test',
|
||||
{ email }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get company settings (public endpoint)
|
||||
*/
|
||||
getCompanySettings: async (): Promise<CompanySettingsResponse> => {
|
||||
const response = await apiClient.get<CompanySettingsResponse>(
|
||||
'/api/admin/system-settings/company'
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update company settings (Admin only)
|
||||
*/
|
||||
updateCompanySettings: async (
|
||||
settings: UpdateCompanySettingsRequest
|
||||
): Promise<CompanySettingsResponse> => {
|
||||
const response = await apiClient.put<CompanySettingsResponse>(
|
||||
'/api/admin/system-settings/company',
|
||||
settings
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload company logo (Admin only)
|
||||
*/
|
||||
uploadCompanyLogo: async (
|
||||
file: File
|
||||
): Promise<UploadLogoResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const response = await apiClient.post<UploadLogoResponse>(
|
||||
'/api/admin/system-settings/company/logo',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload company favicon (Admin only)
|
||||
*/
|
||||
uploadCompanyFavicon: async (
|
||||
file: File
|
||||
): Promise<UploadFaviconResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const response = await apiClient.post<UploadFaviconResponse>(
|
||||
'/api/admin/system-settings/company/favicon',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default systemSettingsService;
|
||||
|
||||
export type {
|
||||
PlatformCurrencyResponse,
|
||||
UpdateCurrencyRequest,
|
||||
StripeSettingsResponse,
|
||||
UpdateStripeSettingsRequest,
|
||||
SmtpSettingsResponse,
|
||||
UpdateSmtpSettingsRequest,
|
||||
TestSmtpEmailRequest,
|
||||
TestSmtpEmailResponse,
|
||||
CompanySettingsResponse,
|
||||
UpdateCompanySettingsRequest,
|
||||
UploadLogoResponse,
|
||||
UploadFaviconResponse,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user