4425 lines
241 KiB
TypeScript
4425 lines
241 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Home,
|
|
Mail,
|
|
Info,
|
|
FileText,
|
|
Search,
|
|
Save,
|
|
Globe,
|
|
X,
|
|
Plus,
|
|
Trash2,
|
|
Image as ImageIcon,
|
|
Eye,
|
|
Edit,
|
|
Upload,
|
|
Loader2,
|
|
Check,
|
|
XCircle,
|
|
Award,
|
|
Shield,
|
|
RefreshCw,
|
|
Accessibility,
|
|
HelpCircle
|
|
} from 'lucide-react';
|
|
import { pageContentService, PageContent, PageType, UpdatePageContentData, bannerService, Banner } from '../../services/api';
|
|
import { toast } from 'react-toastify';
|
|
import Loading from '../../components/common/Loading';
|
|
import { ConfirmationDialog } from '../../components/common';
|
|
import IconPicker from '../../components/admin/IconPicker';
|
|
|
|
type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo' | 'privacy' | 'terms' | 'refunds' | 'cancellation' | 'accessibility' | 'faq';
|
|
|
|
const PageContentDashboard: React.FC = () => {
|
|
const [activeTab, setActiveTab] = useState<ContentTab>('overview');
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [pageContents, setPageContents] = useState<Record<PageType, PageContent | null>>({
|
|
home: null,
|
|
contact: null,
|
|
about: null,
|
|
footer: null,
|
|
seo: null,
|
|
privacy: null,
|
|
terms: null,
|
|
refunds: null,
|
|
cancellation: null,
|
|
accessibility: null,
|
|
faq: null,
|
|
});
|
|
|
|
// Form states for each page
|
|
const [homeData, setHomeData] = useState<UpdatePageContentData>({});
|
|
const [contactData, setContactData] = useState<UpdatePageContentData>({});
|
|
const [aboutData, setAboutData] = useState<UpdatePageContentData>({});
|
|
const [footerData, setFooterData] = useState<UpdatePageContentData>({});
|
|
const [seoData, setSeoData] = useState<UpdatePageContentData>({});
|
|
const [privacyData, setPrivacyData] = useState<UpdatePageContentData>({ is_active: true });
|
|
const [termsData, setTermsData] = useState<UpdatePageContentData>({ is_active: true });
|
|
const [refundsData, setRefundsData] = useState<UpdatePageContentData>({ is_active: true });
|
|
const [cancellationData, setCancellationData] = useState<UpdatePageContentData>({ is_active: true });
|
|
const [accessibilityData, setAccessibilityData] = useState<UpdatePageContentData>({ is_active: true });
|
|
const [faqData, setFaqData] = useState<UpdatePageContentData>({ is_active: true });
|
|
|
|
// Banner management state
|
|
const [banners, setBanners] = useState<Banner[]>([]);
|
|
const [loadingBanners, setLoadingBanners] = useState(false);
|
|
const [showBannerModal, setShowBannerModal] = useState(false);
|
|
const [editingBanner, setEditingBanner] = useState<Banner | null>(null);
|
|
const [bannerFormData, setBannerFormData] = useState({
|
|
title: '',
|
|
description: '',
|
|
image_url: '',
|
|
link_url: '',
|
|
position: 'home',
|
|
display_order: 0,
|
|
is_active: true,
|
|
start_date: '',
|
|
end_date: '',
|
|
});
|
|
const [imageFile, setImageFile] = useState<File | null>(null);
|
|
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
|
const [uploadingImage, setUploadingImage] = useState(false);
|
|
const [useFileUpload, setUseFileUpload] = useState(true);
|
|
const [deleteConfirm, setDeleteConfirm] = useState<{ show: boolean; id: number | null }>({ show: false, id: null });
|
|
|
|
useEffect(() => {
|
|
fetchAllPageContents();
|
|
if (activeTab === 'home') {
|
|
fetchBanners();
|
|
}
|
|
}, [activeTab]);
|
|
|
|
const fetchAllPageContents = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await pageContentService.getAllPageContents();
|
|
const contents = response.data.page_contents || [];
|
|
|
|
const contentsMap: Record<PageType, PageContent | null> = {
|
|
home: null,
|
|
contact: null,
|
|
about: null,
|
|
footer: null,
|
|
seo: null,
|
|
privacy: null,
|
|
terms: null,
|
|
refunds: null,
|
|
cancellation: null,
|
|
accessibility: null,
|
|
faq: null,
|
|
};
|
|
|
|
contents.forEach((content) => {
|
|
if (content.page_type in contentsMap) {
|
|
contentsMap[content.page_type as PageType] = content;
|
|
}
|
|
});
|
|
|
|
setPageContents(contentsMap);
|
|
|
|
// Initialize form data
|
|
initializeFormData(contentsMap);
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Unable to load page contents');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Helper function to normalize arrays (handle both arrays and JSON strings)
|
|
const normalizeArray = (value: any): any[] => {
|
|
if (!value) return [];
|
|
if (Array.isArray(value)) return value;
|
|
if (typeof value === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
return [];
|
|
};
|
|
|
|
const initializeFormData = (contents: Record<PageType, PageContent | null>) => {
|
|
// Home
|
|
if (contents.home) {
|
|
setHomeData({
|
|
title: contents.home.title || '',
|
|
subtitle: contents.home.subtitle || '',
|
|
description: contents.home.description || '',
|
|
content: contents.home.content || '',
|
|
hero_title: contents.home.hero_title || '',
|
|
hero_subtitle: contents.home.hero_subtitle || '',
|
|
hero_image: contents.home.hero_image || '',
|
|
meta_title: contents.home.meta_title || '',
|
|
meta_description: contents.home.meta_description || '',
|
|
meta_keywords: contents.home.meta_keywords || '',
|
|
og_title: contents.home.og_title || '',
|
|
og_description: contents.home.og_description || '',
|
|
og_image: contents.home.og_image || '',
|
|
features: normalizeArray(contents.home.features),
|
|
amenities_section_title: contents.home.amenities_section_title || '',
|
|
amenities_section_subtitle: contents.home.amenities_section_subtitle || '',
|
|
amenities: normalizeArray(contents.home.amenities),
|
|
testimonials_section_title: contents.home.testimonials_section_title || '',
|
|
testimonials_section_subtitle: contents.home.testimonials_section_subtitle || '',
|
|
testimonials: normalizeArray(contents.home.testimonials),
|
|
about_preview_title: contents.home.about_preview_title || '',
|
|
about_preview_subtitle: contents.home.about_preview_subtitle || '',
|
|
about_preview_content: contents.home.about_preview_content || '',
|
|
about_preview_image: contents.home.about_preview_image || '',
|
|
stats: normalizeArray(contents.home.stats),
|
|
luxury_section_title: contents.home.luxury_section_title || '',
|
|
luxury_section_subtitle: contents.home.luxury_section_subtitle || '',
|
|
luxury_section_image: contents.home.luxury_section_image || '',
|
|
luxury_features: normalizeArray(contents.home.luxury_features),
|
|
luxury_gallery_section_title: contents.home.luxury_gallery_section_title || '',
|
|
luxury_gallery_section_subtitle: contents.home.luxury_gallery_section_subtitle || '',
|
|
luxury_gallery: normalizeArray(contents.home.luxury_gallery),
|
|
luxury_testimonials_section_title: contents.home.luxury_testimonials_section_title || '',
|
|
luxury_testimonials_section_subtitle: contents.home.luxury_testimonials_section_subtitle || '',
|
|
luxury_testimonials: normalizeArray(contents.home.luxury_testimonials),
|
|
luxury_services_section_title: contents.home.luxury_services_section_title || '',
|
|
luxury_services_section_subtitle: contents.home.luxury_services_section_subtitle || '',
|
|
luxury_services: normalizeArray(contents.home.luxury_services),
|
|
luxury_experiences_section_title: contents.home.luxury_experiences_section_title || '',
|
|
luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '',
|
|
luxury_experiences: normalizeArray(contents.home.luxury_experiences),
|
|
awards_section_title: contents.home.awards_section_title || '',
|
|
awards_section_subtitle: contents.home.awards_section_subtitle || '',
|
|
awards: normalizeArray(contents.home.awards),
|
|
cta_title: contents.home.cta_title || '',
|
|
cta_subtitle: contents.home.cta_subtitle || '',
|
|
cta_button_text: contents.home.cta_button_text || '',
|
|
cta_button_link: contents.home.cta_button_link || '',
|
|
cta_image: contents.home.cta_image || '',
|
|
partners_section_title: contents.home.partners_section_title || '',
|
|
partners_section_subtitle: contents.home.partners_section_subtitle || '',
|
|
partners: normalizeArray(contents.home.partners),
|
|
});
|
|
}
|
|
|
|
// Contact
|
|
if (contents.contact) {
|
|
setContactData({
|
|
title: contents.contact.title || '',
|
|
subtitle: contents.contact.subtitle || '',
|
|
description: contents.contact.description || '',
|
|
content: contents.contact.content || '',
|
|
map_url: contents.contact.map_url || '',
|
|
meta_title: contents.contact.meta_title || '',
|
|
meta_description: contents.contact.meta_description || '',
|
|
});
|
|
}
|
|
|
|
// About
|
|
if (contents.about) {
|
|
setAboutData({
|
|
title: contents.about.title || '',
|
|
subtitle: contents.about.subtitle || '',
|
|
description: contents.about.description || '',
|
|
content: contents.about.content || '',
|
|
story_content: contents.about.story_content || '',
|
|
values: normalizeArray(contents.about.values),
|
|
features: normalizeArray(contents.about.features),
|
|
about_hero_image: contents.about.about_hero_image || '',
|
|
mission: contents.about.mission || '',
|
|
vision: contents.about.vision || '',
|
|
team: normalizeArray(contents.about.team),
|
|
timeline: normalizeArray(contents.about.timeline),
|
|
achievements: normalizeArray(contents.about.achievements),
|
|
meta_title: contents.about.meta_title || '',
|
|
meta_description: contents.about.meta_description || '',
|
|
});
|
|
}
|
|
|
|
// Footer
|
|
if (contents.footer) {
|
|
setFooterData({
|
|
title: contents.footer.title || '',
|
|
description: contents.footer.description || '',
|
|
social_links: contents.footer.social_links || {},
|
|
footer_links: contents.footer.footer_links || { quick_links: [], support_links: [] },
|
|
badges: contents.footer.badges || [],
|
|
copyright_text: contents.footer.copyright_text || '',
|
|
meta_title: contents.footer.meta_title || '',
|
|
meta_description: contents.footer.meta_description || '',
|
|
});
|
|
}
|
|
|
|
// SEO
|
|
if (contents.seo) {
|
|
setSeoData({
|
|
meta_title: contents.seo.meta_title || '',
|
|
meta_description: contents.seo.meta_description || '',
|
|
meta_keywords: contents.seo.meta_keywords || '',
|
|
og_title: contents.seo.og_title || '',
|
|
og_description: contents.seo.og_description || '',
|
|
og_image: contents.seo.og_image || '',
|
|
canonical_url: contents.seo.canonical_url || '',
|
|
});
|
|
}
|
|
|
|
// Privacy
|
|
if (contents.privacy) {
|
|
setPrivacyData({
|
|
title: contents.privacy.title || '',
|
|
subtitle: contents.privacy.subtitle || '',
|
|
description: contents.privacy.description || '',
|
|
content: contents.privacy.content || '',
|
|
meta_title: contents.privacy.meta_title || '',
|
|
meta_description: contents.privacy.meta_description || '',
|
|
is_active: contents.privacy.is_active ?? true,
|
|
});
|
|
}
|
|
|
|
// Terms
|
|
if (contents.terms) {
|
|
setTermsData({
|
|
title: contents.terms.title || '',
|
|
subtitle: contents.terms.subtitle || '',
|
|
description: contents.terms.description || '',
|
|
content: contents.terms.content || '',
|
|
meta_title: contents.terms.meta_title || '',
|
|
meta_description: contents.terms.meta_description || '',
|
|
is_active: contents.terms.is_active ?? true,
|
|
});
|
|
}
|
|
|
|
// Refunds
|
|
if (contents.refunds) {
|
|
setRefundsData({
|
|
title: contents.refunds.title || '',
|
|
subtitle: contents.refunds.subtitle || '',
|
|
description: contents.refunds.description || '',
|
|
content: contents.refunds.content || '',
|
|
meta_title: contents.refunds.meta_title || '',
|
|
meta_description: contents.refunds.meta_description || '',
|
|
is_active: contents.refunds.is_active ?? true,
|
|
});
|
|
}
|
|
|
|
// Cancellation
|
|
if (contents.cancellation) {
|
|
setCancellationData({
|
|
title: contents.cancellation.title || '',
|
|
subtitle: contents.cancellation.subtitle || '',
|
|
description: contents.cancellation.description || '',
|
|
content: contents.cancellation.content || '',
|
|
meta_title: contents.cancellation.meta_title || '',
|
|
meta_description: contents.cancellation.meta_description || '',
|
|
is_active: contents.cancellation.is_active ?? true,
|
|
});
|
|
}
|
|
|
|
// Accessibility
|
|
if (contents.accessibility) {
|
|
setAccessibilityData({
|
|
title: contents.accessibility.title || '',
|
|
subtitle: contents.accessibility.subtitle || '',
|
|
description: contents.accessibility.description || '',
|
|
content: contents.accessibility.content || '',
|
|
meta_title: contents.accessibility.meta_title || '',
|
|
meta_description: contents.accessibility.meta_description || '',
|
|
is_active: contents.accessibility.is_active ?? true,
|
|
});
|
|
}
|
|
|
|
// FAQ
|
|
if (contents.faq) {
|
|
setFaqData({
|
|
title: contents.faq.title || '',
|
|
subtitle: contents.faq.subtitle || '',
|
|
description: contents.faq.description || '',
|
|
content: contents.faq.content || '',
|
|
meta_title: contents.faq.meta_title || '',
|
|
meta_description: contents.faq.meta_description || '',
|
|
is_active: contents.faq.is_active ?? true,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleSave = async (pageType: PageType, data: UpdatePageContentData) => {
|
|
try {
|
|
setSaving(true);
|
|
// 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) {
|
|
toast.error(error.response?.data?.message || `Failed to save ${pageType} content`);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
// Banner management functions
|
|
const fetchBanners = async () => {
|
|
try {
|
|
setLoadingBanners(true);
|
|
const response = await bannerService.getAllBanners({ position: 'home' });
|
|
if (response.success || response.status === 'success') {
|
|
setBanners(response.data?.banners || []);
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Failed to load banners');
|
|
} finally {
|
|
setLoadingBanners(false);
|
|
}
|
|
};
|
|
|
|
const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
toast.error('Image size must be less than 5MB');
|
|
return;
|
|
}
|
|
|
|
setImageFile(file);
|
|
|
|
// Create preview
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setImagePreview(reader.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
|
|
// Upload image immediately
|
|
try {
|
|
setUploadingImage(true);
|
|
const response = await bannerService.uploadBannerImage(file);
|
|
if (response.success) {
|
|
setBannerFormData({ ...bannerFormData, image_url: response.data.image_url });
|
|
toast.success('Image uploaded successfully');
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Failed to upload image');
|
|
setImageFile(null);
|
|
setImagePreview(null);
|
|
} finally {
|
|
setUploadingImage(false);
|
|
}
|
|
};
|
|
|
|
// Generic image upload handler for page content images
|
|
const handlePageContentImageUpload = async (
|
|
file: File,
|
|
onSuccess: (imageUrl: string) => void
|
|
) => {
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
toast.error('Image size must be less than 5MB');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await pageContentService.uploadImage(file);
|
|
if (response.success) {
|
|
onSuccess(response.data.image_url);
|
|
toast.success('Image uploaded successfully');
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Failed to upload image');
|
|
}
|
|
};
|
|
|
|
const handleBannerSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!bannerFormData.image_url && !imageFile) {
|
|
toast.error('Please upload an image or provide an image URL');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let imageUrl = bannerFormData.image_url;
|
|
if (imageFile && !imageUrl) {
|
|
setUploadingImage(true);
|
|
const uploadResponse = await bannerService.uploadBannerImage(imageFile);
|
|
if (uploadResponse.success) {
|
|
imageUrl = uploadResponse.data.image_url;
|
|
} else {
|
|
throw new Error('Failed to upload image');
|
|
}
|
|
setUploadingImage(false);
|
|
}
|
|
|
|
const submitData = {
|
|
...bannerFormData,
|
|
image_url: imageUrl,
|
|
start_date: bannerFormData.start_date || undefined,
|
|
end_date: bannerFormData.end_date || undefined,
|
|
};
|
|
|
|
if (editingBanner) {
|
|
await bannerService.updateBanner(editingBanner.id, submitData);
|
|
toast.success('Banner updated successfully');
|
|
} else {
|
|
await bannerService.createBanner(submitData);
|
|
toast.success('Banner created successfully');
|
|
}
|
|
setShowBannerModal(false);
|
|
resetBannerForm();
|
|
fetchBanners();
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'An error occurred');
|
|
setUploadingImage(false);
|
|
}
|
|
};
|
|
|
|
const handleEditBanner = (banner: Banner) => {
|
|
setEditingBanner(banner);
|
|
setBannerFormData({
|
|
title: banner.title || '',
|
|
description: banner.description || '',
|
|
image_url: banner.image_url || '',
|
|
link_url: banner.link_url || '',
|
|
position: banner.position || 'home',
|
|
display_order: banner.display_order || 0,
|
|
is_active: banner.is_active ?? true,
|
|
start_date: banner.start_date ? banner.start_date.split('T')[0] : '',
|
|
end_date: banner.end_date ? banner.end_date.split('T')[0] : '',
|
|
});
|
|
setImageFile(null);
|
|
const previewUrl = banner.image_url
|
|
? (banner.image_url.startsWith('http')
|
|
? banner.image_url
|
|
: `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${banner.image_url}`)
|
|
: null;
|
|
setImagePreview(previewUrl);
|
|
setUseFileUpload(false);
|
|
setShowBannerModal(true);
|
|
};
|
|
|
|
const handleDeleteBanner = async () => {
|
|
if (!deleteConfirm.id) return;
|
|
|
|
try {
|
|
await bannerService.deleteBanner(deleteConfirm.id);
|
|
toast.success('Banner deleted successfully');
|
|
setDeleteConfirm({ show: false, id: null });
|
|
fetchBanners();
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Failed to delete banner');
|
|
}
|
|
};
|
|
|
|
const resetBannerForm = () => {
|
|
setBannerFormData({
|
|
title: '',
|
|
description: '',
|
|
image_url: '',
|
|
link_url: '',
|
|
position: 'home',
|
|
display_order: 0,
|
|
is_active: true,
|
|
start_date: '',
|
|
end_date: '',
|
|
});
|
|
setImageFile(null);
|
|
setImagePreview(null);
|
|
setUseFileUpload(true);
|
|
setEditingBanner(null);
|
|
};
|
|
|
|
const toggleBannerActive = async (banner: Banner) => {
|
|
try {
|
|
await bannerService.updateBanner(banner.id, {
|
|
is_active: !banner.is_active,
|
|
});
|
|
toast.success(`Banner ${!banner.is_active ? 'activated' : 'deactivated'}`);
|
|
fetchBanners();
|
|
} catch (error: any) {
|
|
toast.error(error.response?.data?.message || 'Failed to update banner');
|
|
}
|
|
};
|
|
|
|
const tabs = [
|
|
{ id: 'overview' as ContentTab, label: 'Overview', icon: Home },
|
|
{ id: 'home' as ContentTab, label: 'Home', icon: Home },
|
|
{ id: 'contact' as ContentTab, label: 'Contact', icon: Mail },
|
|
{ id: 'about' as ContentTab, label: 'About', icon: Info },
|
|
{ id: 'privacy' as ContentTab, label: 'Privacy', icon: Shield },
|
|
{ id: 'terms' as ContentTab, label: 'Terms', icon: FileText },
|
|
{ id: 'refunds' as ContentTab, label: 'Refunds', icon: RefreshCw },
|
|
{ id: 'cancellation' as ContentTab, label: 'Cancellation', icon: XCircle },
|
|
{ id: 'accessibility' as ContentTab, label: 'Accessibility', icon: Accessibility },
|
|
{ id: 'faq' as ContentTab, label: 'FAQ', icon: HelpCircle },
|
|
{ id: 'footer' as ContentTab, label: 'Footer', icon: FileText },
|
|
{ id: 'seo' as ContentTab, label: 'SEO', icon: Search },
|
|
];
|
|
|
|
if (loading) {
|
|
return <Loading />;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50">
|
|
<div className="max-w-7xl mx-auto px-2 sm:px-3 md:px-4 lg:px-6 xl:px-8 py-2 sm:py-4 md:py-6 lg:py-8 space-y-3 sm:space-y-4 md:space-y-6 lg:space-y-8">
|
|
{/* Luxury Header */}
|
|
<div className="relative">
|
|
<div className="absolute inset-0 bg-gradient-to-r from-purple-400/5 via-transparent to-indigo-600/5 rounded-3xl blur-3xl"></div>
|
|
<div className="relative bg-white/80 backdrop-blur-xl rounded-xl sm:rounded-2xl md:rounded-3xl shadow-2xl border border-purple-200/30 p-3 sm:p-4 md:p-6 lg:p-8">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 sm:gap-6 md:gap-8">
|
|
<div className="flex items-start gap-3 sm:gap-4 md:gap-5">
|
|
<div className="relative flex-shrink-0">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-purple-400 to-indigo-600 rounded-xl sm:rounded-2xl blur-lg opacity-50"></div>
|
|
<div className="relative p-2.5 sm:p-3 md:p-4 rounded-xl sm:rounded-2xl bg-gradient-to-br from-purple-500 via-purple-500 to-indigo-600 shadow-xl border border-purple-400/50">
|
|
<Globe className="w-5 h-5 sm:w-6 sm:h-6 md:w-8 md:h-8 text-white" />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2 sm:space-y-3 flex-1">
|
|
<div className="flex items-center gap-2 sm:gap-3 flex-wrap">
|
|
<h1 className="text-2xl sm:text-3xl md:text-3xl font-extrabold bg-gradient-to-r from-slate-900 via-purple-700 to-slate-900 bg-clip-text text-transparent">
|
|
Page Content Management
|
|
</h1>
|
|
</div>
|
|
<p className="text-gray-600 text-xs sm:text-sm md:text-sm max-w-2xl leading-relaxed">
|
|
Centralized control for all frontend pages and SEO optimization
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Premium Tab Navigation */}
|
|
<div className="mt-4 sm:mt-6 md:mt-8 lg:mt-10 pt-4 sm:pt-6 md:pt-8 border-t border-gradient-to-r from-transparent via-purple-200/30 to-transparent">
|
|
<div className="overflow-x-auto -mx-2 sm:-mx-3 px-2 sm:px-3 scrollbar-hide">
|
|
<div className="flex gap-2 sm:gap-3 min-w-max sm:min-w-0 sm:flex-wrap">
|
|
{tabs.map((tab) => {
|
|
const Icon = tab.icon;
|
|
const isActive = activeTab === tab.id;
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`
|
|
group relative flex items-center gap-1.5 sm:gap-2 md:gap-3 px-3 sm:px-4 md:px-6 py-2 sm:py-2.5 md:py-3.5 rounded-lg sm:rounded-xl font-semibold text-xs sm:text-sm flex-shrink-0
|
|
transition-all duration-300 overflow-hidden
|
|
${
|
|
isActive
|
|
? 'bg-gradient-to-r from-purple-500 via-purple-500 to-indigo-600 text-white shadow-xl shadow-purple-500/40 scale-105'
|
|
: 'bg-white/80 text-gray-700 border border-gray-200/60 hover:border-purple-300/60 hover:bg-gradient-to-r hover:from-purple-50/50 hover:to-indigo-50/30 hover:shadow-lg hover:scale-102'
|
|
}
|
|
`}
|
|
>
|
|
{isActive && (
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer"></div>
|
|
)}
|
|
<Icon className={`w-3.5 h-3.5 sm:w-4 sm:h-4 md:w-5 md:h-5 transition-transform duration-300 flex-shrink-0 ${isActive ? 'text-white' : 'text-gray-600 group-hover:text-purple-600 group-hover:scale-110'}`} />
|
|
<span className="relative z-10 whitespace-nowrap">{tab.label}</span>
|
|
{isActive && (
|
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 sm:h-1 bg-gradient-to-r from-purple-300 via-purple-400 to-indigo-400"></div>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Overview Tab */}
|
|
{activeTab === 'overview' && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5 md:gap-6 lg:gap-8">
|
|
{[
|
|
{ id: 'home' as PageType, label: 'Home Page', icon: Home, color: 'blue', description: 'Manage hero section, featured content' },
|
|
{ id: 'contact' as PageType, label: 'Contact Page', icon: Mail, color: 'green', description: 'Manage contact information and form' },
|
|
{ id: 'about' as PageType, label: 'About Page', icon: Info, color: 'amber', description: 'Manage story, values, and features' },
|
|
{ id: 'privacy' as PageType, label: 'Privacy Policy', icon: Shield, color: 'red', description: 'Manage privacy policy content' },
|
|
{ id: 'terms' as PageType, label: 'Terms & Conditions', icon: FileText, color: 'teal', description: 'Manage terms and conditions' },
|
|
{ id: 'refunds' as PageType, label: 'Refunds Policy', icon: RefreshCw, color: 'orange', description: 'Manage refunds policy content' },
|
|
{ id: 'cancellation' as PageType, label: 'Cancellation Policy', icon: XCircle, color: 'pink', description: 'Manage cancellation policy content' },
|
|
{ id: 'accessibility' as PageType, label: 'Accessibility', icon: Accessibility, color: 'cyan', description: 'Manage accessibility information' },
|
|
{ id: 'faq' as PageType, label: 'FAQ', icon: HelpCircle, color: 'violet', description: 'Manage frequently asked questions' },
|
|
{ id: 'footer' as PageType, label: 'Footer', icon: FileText, color: 'purple', description: 'Manage footer links and social media' },
|
|
{ id: 'seo' as PageType, label: 'SEO Settings', icon: Search, color: 'indigo', description: 'Manage meta tags and SEO optimization' },
|
|
].map((page) => {
|
|
const Icon = page.icon;
|
|
const hasContent = pageContents[page.id] !== null;
|
|
return (
|
|
<div
|
|
key={page.id}
|
|
onClick={() => setActiveTab(page.id as ContentTab)}
|
|
className={`group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 overflow-hidden ${
|
|
page.color === 'blue' ? 'border-blue-100/50 hover:border-blue-300/60' :
|
|
page.color === 'green' ? 'border-green-100/50 hover:border-green-300/60' :
|
|
page.color === 'amber' ? 'border-amber-100/50 hover:border-amber-300/60' :
|
|
page.color === 'purple' ? 'border-purple-100/50 hover:border-purple-300/60' :
|
|
page.color === 'red' ? 'border-red-100/50 hover:border-red-300/60' :
|
|
page.color === 'teal' ? 'border-teal-100/50 hover:border-teal-300/60' :
|
|
page.color === 'orange' ? 'border-orange-100/50 hover:border-orange-300/60' :
|
|
'border-indigo-100/50 hover:border-indigo-300/60'
|
|
}`}
|
|
>
|
|
<div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-br opacity-10 to-transparent rounded-bl-full ${
|
|
page.color === 'blue' ? 'from-blue-400' :
|
|
page.color === 'green' ? 'from-green-400' :
|
|
page.color === 'amber' ? 'from-amber-400' :
|
|
page.color === 'purple' ? 'from-purple-400' :
|
|
page.color === 'red' ? 'from-red-400' :
|
|
page.color === 'teal' ? 'from-teal-400' :
|
|
page.color === 'orange' ? 'from-orange-400' :
|
|
'from-indigo-400'
|
|
}`}></div>
|
|
<div className="relative space-y-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className={`relative p-3.5 rounded-xl shadow-lg border group-hover:scale-110 transition-transform ${
|
|
page.color === 'blue' ? 'bg-gradient-to-br from-blue-500 to-blue-600 border-blue-400/50' :
|
|
page.color === 'green' ? 'bg-gradient-to-br from-green-500 to-green-600 border-green-400/50' :
|
|
page.color === 'amber' ? 'bg-gradient-to-br from-amber-500 to-amber-600 border-amber-400/50' :
|
|
page.color === 'purple' ? 'bg-gradient-to-br from-purple-500 to-purple-600 border-purple-400/50' :
|
|
page.color === 'red' ? 'bg-gradient-to-br from-red-500 to-red-600 border-red-400/50' :
|
|
page.color === 'teal' ? 'bg-gradient-to-br from-teal-500 to-teal-600 border-teal-400/50' :
|
|
page.color === 'orange' ? 'bg-gradient-to-br from-orange-500 to-orange-600 border-orange-400/50' :
|
|
'bg-gradient-to-br from-indigo-500 to-indigo-600 border-indigo-400/50'
|
|
}`}>
|
|
<Icon className="w-6 h-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900 text-xl mb-1">{page.label}</h3>
|
|
<div className={`h-1 w-12 rounded-full ${
|
|
page.color === 'blue' ? 'bg-gradient-to-r from-blue-500 to-blue-600' :
|
|
page.color === 'red' ? 'bg-gradient-to-r from-red-500 to-red-600' :
|
|
page.color === 'teal' ? 'bg-gradient-to-r from-teal-500 to-teal-600' :
|
|
page.color === 'orange' ? 'bg-gradient-to-r from-orange-500 to-orange-600' :
|
|
page.color === 'green' ? 'bg-gradient-to-r from-green-500 to-green-600' :
|
|
page.color === 'amber' ? 'bg-gradient-to-r from-amber-500 to-amber-600' :
|
|
page.color === 'purple' ? 'bg-gradient-to-r from-purple-500 to-purple-600' :
|
|
'bg-gradient-to-r from-indigo-500 to-indigo-600'
|
|
}`}></div>
|
|
</div>
|
|
</div>
|
|
{hasContent && (
|
|
<div className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-xs font-semibold">
|
|
<Eye className="w-3 h-3 inline mr-1" />
|
|
Active
|
|
</div>
|
|
)}
|
|
</div>
|
|
<p className="text-gray-600 text-sm leading-relaxed">
|
|
{page.description}
|
|
</p>
|
|
<div className="pt-5 border-t border-gray-100">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-gray-500 font-medium">{hasContent ? 'Edit Content' : 'Add Content'}</span>
|
|
<span className={`text-sm font-semibold ${
|
|
page.color === 'blue' ? 'text-blue-600' :
|
|
page.color === 'green' ? 'text-green-600' :
|
|
page.color === 'amber' ? 'text-amber-600' :
|
|
page.color === 'purple' ? 'text-purple-600' :
|
|
'text-indigo-600'
|
|
}`}>→</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* Home Tab */}
|
|
{activeTab === 'home' && (
|
|
<div className="space-y-8">
|
|
{/* Home Page Content Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-3xl font-extrabold text-gray-900 mb-6">Home Page Content</h2>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Hero Title</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.hero_title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, hero_title: 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"
|
|
placeholder="Welcome to Luxury Hotel"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Hero Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.hero_subtitle || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, hero_subtitle: 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"
|
|
placeholder="Experience unparalleled luxury"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Hero Image</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
value={homeData.hero_image || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, hero_image: e.target.value })}
|
|
className="flex-1 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="https://example.com/hero-image.jpg or upload"
|
|
/>
|
|
<label className="px-4 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all cursor-pointer flex items-center gap-2">
|
|
<Upload className="w-4 h-4" />
|
|
Upload
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setHomeData((prevData) => ({ ...prevData, hero_image: imageUrl }));
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{homeData.hero_image && (
|
|
<div className="mt-2">
|
|
<img src={homeData.hero_image.startsWith('http') ? homeData.hero_image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${homeData.hero_image}`} alt="Hero preview" className="max-w-full max-h-48 rounded-lg border border-gray-200" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Page Title</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, title: 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">Description</label>
|
|
<textarea
|
|
value={homeData.description || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, description: e.target.value })}
|
|
rows={4}
|
|
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="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>
|
|
<input
|
|
type="text"
|
|
value={homeData.meta_title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, meta_title: 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"
|
|
placeholder="SEO Meta Title"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">OG Image URL</label>
|
|
<input
|
|
type="url"
|
|
value={homeData.og_image || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, og_image: 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>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Description</label>
|
|
<textarea
|
|
value={homeData.meta_description || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, meta_description: 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="SEO Meta Description"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Amenities Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-2xl font-extrabold text-gray-900 mb-6">Amenities Section</h2>
|
|
<div className="space-y-6 mb-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Title</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.amenities_section_title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, amenities_section_title: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Luxury Amenities"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.amenities_section_subtitle || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, amenities_section_subtitle: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Experience world-class amenities"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Amenities</h3>
|
|
<button
|
|
onClick={() => {
|
|
setHomeData((prevData) => {
|
|
const current = Array.isArray(prevData.amenities) ? prevData.amenities : [];
|
|
return {
|
|
...prevData,
|
|
amenities: [...current, { icon: 'Sparkles', title: '', description: '', image: '' }]
|
|
};
|
|
});
|
|
}}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Amenity
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{Array.isArray(homeData.amenities) && homeData.amenities.map((amenity, index) => (
|
|
<div key={`amenity-${index}-${amenity.title || index}`} className="p-6 border-2 border-gray-200 rounded-xl space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Amenity {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setHomeData((prevData) => {
|
|
const currentAmenities = Array.isArray(prevData.amenities) ? prevData.amenities : [];
|
|
const updated = currentAmenities.filter((_, i) => i !== index);
|
|
return { ...prevData, amenities: updated };
|
|
});
|
|
}}
|
|
className="text-red-600 hover:text-red-700 p-1"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<IconPicker
|
|
value={amenity?.icon || ''}
|
|
onChange={(iconName) => {
|
|
setHomeData((prevData) => {
|
|
const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
|
|
currentAmenities[index] = { ...currentAmenities[index], icon: iconName };
|
|
return { ...prevData, amenities: currentAmenities };
|
|
});
|
|
}}
|
|
label="Icon"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Image (optional)</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
value={amenity?.image || ''}
|
|
onChange={(e) => {
|
|
setHomeData((prevData) => {
|
|
const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
|
|
currentAmenities[index] = { ...currentAmenities[index], image: e.target.value };
|
|
return { ...prevData, amenities: currentAmenities };
|
|
});
|
|
}}
|
|
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="URL or upload"
|
|
/>
|
|
<label className="px-3 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
|
<Upload className="w-3 h-3" />
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setHomeData((prevData) => {
|
|
const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
|
|
currentAmenities[index] = { ...currentAmenities[index], image: imageUrl };
|
|
return { ...prevData, amenities: currentAmenities };
|
|
});
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{amenity?.image && (
|
|
<div className="mt-2">
|
|
<img src={amenity.image.startsWith('http') ? amenity.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${amenity.image}`} alt="Amenity preview" className="max-w-full max-h-32 rounded-lg border border-gray-200" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={amenity?.title || ''}
|
|
onChange={(e) => {
|
|
setHomeData((prevData) => {
|
|
const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
|
|
currentAmenities[index] = { ...currentAmenities[index], title: e.target.value };
|
|
return { ...prevData, amenities: currentAmenities };
|
|
});
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Description</label>
|
|
<textarea
|
|
value={amenity?.description || ''}
|
|
onChange={(e) => {
|
|
setHomeData((prevData) => {
|
|
const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
|
|
currentAmenities[index] = { ...currentAmenities[index], description: e.target.value };
|
|
return { ...prevData, amenities: currentAmenities };
|
|
});
|
|
}}
|
|
rows={2}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{(!homeData.amenities || homeData.amenities.length === 0) && (
|
|
<p className="text-gray-500 text-center py-8">No amenities added yet. Click "Add Amenity" to get started.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Luxury Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-2xl font-extrabold text-gray-900 mb-6">Luxury Section</h2>
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Title</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.luxury_section_title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, luxury_section_title: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Experience Unparalleled Luxury"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.luxury_section_subtitle || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, luxury_section_subtitle: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Where elegance meets comfort in every detail"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Image</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
value={homeData.luxury_section_image || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, luxury_section_image: e.target.value })}
|
|
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="https://example.com/luxury-image.jpg or upload"
|
|
/>
|
|
<label className="px-4 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-2">
|
|
<Upload className="w-4 h-4" />
|
|
Upload
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setHomeData((prevData) => ({ ...prevData, luxury_section_image: imageUrl }));
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{homeData.luxury_section_image && (
|
|
<div className="mt-2">
|
|
<img src={homeData.luxury_section_image.startsWith('http') ? homeData.luxury_section_image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${homeData.luxury_section_image}`} alt="Luxury section preview" className="max-w-full max-h-48 rounded-lg border border-gray-200" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Luxury Features Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-extrabold text-gray-900">Luxury Features</h2>
|
|
<button
|
|
onClick={() => {
|
|
setHomeData((prevData) => {
|
|
const currentFeatures = Array.isArray(prevData.luxury_features) ? prevData.luxury_features : [];
|
|
return {
|
|
...prevData,
|
|
luxury_features: [...currentFeatures, { icon: 'Sparkles', title: '', description: '' }]
|
|
};
|
|
});
|
|
}}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Luxury Feature
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{Array.isArray(homeData.luxury_features) && homeData.luxury_features.map((feature, index) => (
|
|
<div key={`luxury-feature-${index}-${feature.title || index}`} className="p-6 border-2 border-gray-200 rounded-xl space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h3 className="font-semibold text-gray-900">Luxury Feature {index + 1}</h3>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setHomeData((prevData) => {
|
|
const currentFeatures = Array.isArray(prevData.luxury_features) ? prevData.luxury_features : [];
|
|
const updated = currentFeatures.filter((_, i) => i !== index);
|
|
return { ...prevData, luxury_features: updated };
|
|
});
|
|
}}
|
|
className="text-red-600 hover:text-red-700 p-1"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<IconPicker
|
|
value={feature?.icon || ''}
|
|
onChange={(iconName) => {
|
|
setHomeData((prevData) => {
|
|
const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
|
|
currentFeatures[index] = { ...currentFeatures[index], icon: iconName };
|
|
return { ...prevData, luxury_features: currentFeatures };
|
|
});
|
|
}}
|
|
label="Icon"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={feature?.title || ''}
|
|
onChange={(e) => {
|
|
setHomeData((prevData) => {
|
|
const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
|
|
currentFeatures[index] = { ...currentFeatures[index], title: e.target.value };
|
|
return { ...prevData, luxury_features: currentFeatures };
|
|
});
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="Feature Title"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Description</label>
|
|
<textarea
|
|
value={feature?.description || ''}
|
|
onChange={(e) => {
|
|
setHomeData((prevData) => {
|
|
const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
|
|
currentFeatures[index] = { ...currentFeatures[index], description: e.target.value };
|
|
return { ...prevData, luxury_features: currentFeatures };
|
|
});
|
|
}}
|
|
rows={2}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="Feature description"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{(!homeData.luxury_features || homeData.luxury_features.length === 0) && (
|
|
<p className="text-gray-500 text-center py-8">No luxury features added yet. Click "Add Luxury Feature" to get started.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Luxury Gallery Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-extrabold text-gray-900">Luxury Gallery</h2>
|
|
</div>
|
|
<div className="mb-6 space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Title</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.luxury_gallery_section_title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, luxury_gallery_section_title: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Luxury Gallery"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.luxury_gallery_section_subtitle || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, luxury_gallery_section_subtitle: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Discover our exquisite spaces and amenities"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<button
|
|
onClick={() => {
|
|
setHomeData((prevData) => {
|
|
const currentGallery = Array.isArray(prevData.luxury_gallery) ? prevData.luxury_gallery : [];
|
|
return {
|
|
...prevData,
|
|
luxury_gallery: [...currentGallery, '']
|
|
};
|
|
});
|
|
}}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Gallery Image
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{Array.isArray(homeData.luxury_gallery) && homeData.luxury_gallery.map((image, index) => (
|
|
<div key={`luxury-gallery-${index}-${image || index}`} className="flex gap-2 items-start">
|
|
<input
|
|
type="url"
|
|
value={image || ''}
|
|
onChange={(e) => {
|
|
setHomeData((prevData) => {
|
|
const currentGallery = Array.isArray(prevData.luxury_gallery) ? [...prevData.luxury_gallery] : [];
|
|
currentGallery[index] = e.target.value;
|
|
return { ...prevData, luxury_gallery: currentGallery };
|
|
});
|
|
}}
|
|
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="URL or upload"
|
|
/>
|
|
<label className="px-3 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
|
<Upload className="w-3 h-3" />
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setHomeData((prevData) => {
|
|
const currentGallery = Array.isArray(prevData.luxury_gallery) ? [...prevData.luxury_gallery] : [];
|
|
currentGallery[index] = imageUrl;
|
|
return { ...prevData, luxury_gallery: currentGallery };
|
|
});
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setHomeData((prevData) => {
|
|
const currentGallery = Array.isArray(prevData.luxury_gallery) ? prevData.luxury_gallery : [];
|
|
const updated = currentGallery.filter((_, i) => i !== index);
|
|
return { ...prevData, luxury_gallery: updated };
|
|
});
|
|
}}
|
|
className="text-red-600 hover:text-red-700 p-2"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
{image && (
|
|
<div className="w-20 h-20 flex-shrink-0">
|
|
<img src={image.startsWith('http') ? image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${image}`} alt={`Luxury gallery ${index + 1}`} className="w-full h-full object-cover rounded-lg border border-gray-200" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
{(!homeData.luxury_gallery || homeData.luxury_gallery.length === 0) && (
|
|
<p className="text-gray-500 text-center py-8">No gallery images added yet. Click "Add Gallery Image" to get started.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Luxury Testimonials Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-extrabold text-gray-900">Luxury Testimonials</h2>
|
|
</div>
|
|
<div className="mb-6 space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Title</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.luxury_testimonials_section_title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, luxury_testimonials_section_title: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Guest Experiences"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.luxury_testimonials_section_subtitle || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, luxury_testimonials_section_subtitle: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Hear from our valued guests about their luxury stay"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<button
|
|
onClick={() => {
|
|
setHomeData((prevData) => {
|
|
const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? prevData.luxury_testimonials : [];
|
|
return {
|
|
...prevData,
|
|
luxury_testimonials: [...currentTestimonials, { name: '', title: '', quote: '', image: '' }]
|
|
};
|
|
});
|
|
}}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Luxury Testimonial
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{Array.isArray(homeData.luxury_testimonials) && homeData.luxury_testimonials.map((testimonial, index) => (
|
|
<div key={`luxury-testimonial-${index}-${testimonial.name || index}`} className="p-6 border-2 border-gray-200 rounded-xl space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Luxury Testimonial {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setHomeData((prevData) => {
|
|
const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? prevData.luxury_testimonials : [];
|
|
const updated = currentTestimonials.filter((_, i) => i !== index);
|
|
return { ...prevData, luxury_testimonials: updated };
|
|
});
|
|
}}
|
|
className="text-red-600 hover:text-red-700 p-1"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Name</label>
|
|
<input
|
|
type="text"
|
|
value={testimonial?.name || ''}
|
|
onChange={(e) => {
|
|
setHomeData((prevData) => {
|
|
const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
|
|
currentTestimonials[index] = { ...currentTestimonials[index], name: e.target.value };
|
|
return { ...prevData, luxury_testimonials: currentTestimonials };
|
|
});
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={testimonial?.title || ''}
|
|
onChange={(e) => {
|
|
setHomeData((prevData) => {
|
|
const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
|
|
currentTestimonials[index] = { ...currentTestimonials[index], title: e.target.value };
|
|
return { ...prevData, luxury_testimonials: currentTestimonials };
|
|
});
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Image (optional)</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
value={testimonial?.image || ''}
|
|
onChange={(e) => {
|
|
setHomeData((prevData) => {
|
|
const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
|
|
currentTestimonials[index] = { ...currentTestimonials[index], image: e.target.value };
|
|
return { ...prevData, luxury_testimonials: currentTestimonials };
|
|
});
|
|
}}
|
|
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="URL or upload"
|
|
/>
|
|
<label className="px-3 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
|
<Upload className="w-3 h-3" />
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setHomeData((prevData) => {
|
|
const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
|
|
currentTestimonials[index] = { ...currentTestimonials[index], image: imageUrl };
|
|
return { ...prevData, luxury_testimonials: currentTestimonials };
|
|
});
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{testimonial?.image && (
|
|
<div className="mt-2">
|
|
<img src={testimonial.image.startsWith('http') ? testimonial.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${testimonial.image}`} alt="Testimonial preview" className="w-16 h-16 rounded-full object-cover border border-gray-200" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Quote</label>
|
|
<textarea
|
|
value={testimonial?.quote || ''}
|
|
onChange={(e) => {
|
|
setHomeData((prevData) => {
|
|
const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
|
|
currentTestimonials[index] = { ...currentTestimonials[index], quote: e.target.value };
|
|
return { ...prevData, luxury_testimonials: currentTestimonials };
|
|
});
|
|
}}
|
|
rows={3}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{(!homeData.luxury_testimonials || homeData.luxury_testimonials.length === 0) && (
|
|
<p className="text-gray-500 text-center py-8">No luxury testimonials added yet. Click "Add Luxury Testimonial" to get started.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* About Preview Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-2xl font-extrabold text-gray-900 mb-6">About Preview Section</h2>
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.about_preview_title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, about_preview_title: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="About Our Hotel"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.about_preview_subtitle || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, about_preview_subtitle: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Content</label>
|
|
<textarea
|
|
value={homeData.about_preview_content || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, about_preview_content: e.target.value })}
|
|
rows={6}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Write about your hotel..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">About Preview Image</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
value={homeData.about_preview_image || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, about_preview_image: e.target.value })}
|
|
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="https://example.com/about-image.jpg or upload"
|
|
/>
|
|
<label className="px-4 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-2">
|
|
<Upload className="w-4 h-4" />
|
|
Upload
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setHomeData((prevData) => ({ ...prevData, about_preview_image: imageUrl }));
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{homeData.about_preview_image && (
|
|
<div className="mt-2">
|
|
<img src={homeData.about_preview_image.startsWith('http') ? homeData.about_preview_image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${homeData.about_preview_image}`} alt="About preview" className="max-w-full max-h-48 rounded-lg border border-gray-200" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-extrabold text-gray-900">Statistics Section</h2>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const current = Array.isArray(homeData.stats) ? homeData.stats : [];
|
|
setHomeData({
|
|
...homeData,
|
|
stats: [...current, { number: '', label: '', icon: 'BarChart' }]
|
|
});
|
|
}}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Stat
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{Array.isArray(homeData.stats) && homeData.stats.map((stat, index) => (
|
|
<div key={`stat-${index}-${stat.label || index}`} className="p-6 border-2 border-gray-200 rounded-xl space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Stat {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const currentStats = Array.isArray(homeData.stats) ? homeData.stats : [];
|
|
const updated = currentStats.filter((_, i) => i !== index);
|
|
setHomeData({ ...homeData, stats: updated });
|
|
}}
|
|
className="text-red-600 hover:text-red-700 p-1"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Number</label>
|
|
<input
|
|
type="text"
|
|
value={stat?.number || ''}
|
|
onChange={(e) => {
|
|
const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
|
|
currentStats[index] = { ...currentStats[index], number: e.target.value };
|
|
setHomeData({ ...homeData, stats: currentStats });
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="1000+"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Label</label>
|
|
<input
|
|
type="text"
|
|
value={stat?.label || ''}
|
|
onChange={(e) => {
|
|
const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
|
|
currentStats[index] = { ...currentStats[index], label: e.target.value };
|
|
setHomeData({ ...homeData, stats: currentStats });
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="Happy Guests"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<IconPicker
|
|
value={stat?.icon || ''}
|
|
onChange={(iconName) => {
|
|
const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
|
|
currentStats[index] = { ...currentStats[index], icon: iconName };
|
|
setHomeData({ ...homeData, stats: currentStats });
|
|
}}
|
|
label="Icon"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{(!homeData.stats || homeData.stats.length === 0) && (
|
|
<p className="text-gray-500 text-center py-8">No statistics added yet. Click "Add Stat" to get started.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Luxury Services Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-extrabold text-gray-900">Luxury Services</h2>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const current = Array.isArray(homeData.luxury_services) ? homeData.luxury_services : [];
|
|
setHomeData((prevData) => ({
|
|
...prevData,
|
|
luxury_services: [...current, { icon: 'Sparkles', title: '', description: '', image: '' }]
|
|
}));
|
|
}}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Service
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{Array.isArray(homeData.luxury_services) && homeData.luxury_services.map((service, index) => (
|
|
<div key={`service-${index}-${service.title || index}`} className="p-6 border-2 border-gray-200 rounded-xl space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Service {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const current = Array.isArray(homeData.luxury_services) ? homeData.luxury_services : [];
|
|
const updated = current.filter((_, i) => i !== index);
|
|
setHomeData((prevData) => ({ ...prevData, luxury_services: updated }));
|
|
}}
|
|
className="text-red-600 hover:text-red-700 p-1"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<IconPicker
|
|
value={service?.icon || ''}
|
|
onChange={(iconName) => {
|
|
const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
|
|
current[index] = { ...current[index], icon: iconName };
|
|
setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
|
|
}}
|
|
label="Icon"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Image</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
value={service?.image || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
|
|
current[index] = { ...current[index], image: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
|
|
}}
|
|
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="URL or upload"
|
|
/>
|
|
<label className="px-3 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
|
<Upload className="w-3 h-3" />
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setHomeData((prevData) => {
|
|
const current = Array.isArray(prevData.luxury_services) ? [...prevData.luxury_services] : [];
|
|
current[index] = { ...current[index], image: imageUrl };
|
|
return { ...prevData, luxury_services: current };
|
|
});
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{service?.image && (
|
|
<div className="mt-2">
|
|
<img src={service.image.startsWith('http') ? service.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${service.image}`} alt="Service preview" className="max-w-full max-h-32 rounded-lg border border-gray-200" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={service?.title || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
|
|
current[index] = { ...current[index], title: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="Service Title"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Description</label>
|
|
<textarea
|
|
value={service?.description || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
|
|
current[index] = { ...current[index], description: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
|
|
}}
|
|
rows={3}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="Service description"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{(!homeData.luxury_services || homeData.luxury_services.length === 0) && (
|
|
<p className="text-gray-500 text-center py-8">No services added yet. Click "Add Service" to get started.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Luxury Experiences Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-extrabold text-gray-900">Luxury Experiences</h2>
|
|
</div>
|
|
<div className="mb-6 space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Title</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.luxury_experiences_section_title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, luxury_experiences_section_title: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Unique Experiences"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.luxury_experiences_section_subtitle || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, luxury_experiences_section_subtitle: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Unforgettable moments await"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const current = Array.isArray(homeData.luxury_experiences) ? homeData.luxury_experiences : [];
|
|
setHomeData((prevData) => ({
|
|
...prevData,
|
|
luxury_experiences: [...current, { icon: 'Star', title: '', description: '', image: '' }]
|
|
}));
|
|
}}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Experience
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{Array.isArray(homeData.luxury_experiences) && homeData.luxury_experiences.map((experience, index) => (
|
|
<div key={`experience-${index}-${experience.title || index}`} className="p-6 border-2 border-gray-200 rounded-xl space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Experience {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const current = Array.isArray(homeData.luxury_experiences) ? homeData.luxury_experiences : [];
|
|
const updated = current.filter((_, i) => i !== index);
|
|
setHomeData((prevData) => ({ ...prevData, luxury_experiences: updated }));
|
|
}}
|
|
className="text-red-600 hover:text-red-700 p-1"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<IconPicker
|
|
value={experience?.icon || ''}
|
|
onChange={(iconName) => {
|
|
const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
|
|
current[index] = { ...current[index], icon: iconName };
|
|
setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
|
|
}}
|
|
label="Icon"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Image</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
value={experience?.image || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
|
|
current[index] = { ...current[index], image: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
|
|
}}
|
|
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="URL or upload"
|
|
/>
|
|
<label className="px-3 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
|
<Upload className="w-3 h-3" />
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setHomeData((prevData) => {
|
|
const current = Array.isArray(prevData.luxury_experiences) ? [...prevData.luxury_experiences] : [];
|
|
current[index] = { ...current[index], image: imageUrl };
|
|
return { ...prevData, luxury_experiences: current };
|
|
});
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{experience?.image && (
|
|
<div className="mt-2">
|
|
<img src={experience.image.startsWith('http') ? experience.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${experience.image}`} alt="Experience preview" className="max-w-full max-h-32 rounded-lg border border-gray-200" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={experience?.title || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
|
|
current[index] = { ...current[index], title: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="Experience Title"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Description</label>
|
|
<textarea
|
|
value={experience?.description || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
|
|
current[index] = { ...current[index], description: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
|
|
}}
|
|
rows={3}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="Experience description"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{(!homeData.luxury_experiences || homeData.luxury_experiences.length === 0) && (
|
|
<p className="text-gray-500 text-center py-8">No experiences added yet. Click "Add Experience" to get started.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Awards Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-extrabold text-gray-900">Awards & Certifications</h2>
|
|
</div>
|
|
<div className="mb-6 space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Title</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.awards_section_title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, awards_section_title: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Awards & Recognition"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.awards_section_subtitle || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, awards_section_subtitle: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Recognized excellence in hospitality"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const current = Array.isArray(homeData.awards) ? homeData.awards : [];
|
|
setHomeData((prevData) => ({
|
|
...prevData,
|
|
awards: [...current, { icon: 'Award', title: '', description: '', image: '', year: '' }]
|
|
}));
|
|
}}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Award
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{Array.isArray(homeData.awards) && homeData.awards.map((award, index) => (
|
|
<div key={`award-${index}-${award.title || index}`} className="p-6 border-2 border-gray-200 rounded-xl space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Award {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const current = Array.isArray(homeData.awards) ? homeData.awards : [];
|
|
const updated = current.filter((_, i) => i !== index);
|
|
setHomeData((prevData) => ({ ...prevData, awards: updated }));
|
|
}}
|
|
className="text-red-600 hover:text-red-700 p-1"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<IconPicker
|
|
value={award?.icon || ''}
|
|
onChange={(iconName) => {
|
|
const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
|
|
current[index] = { ...current[index], icon: iconName };
|
|
setHomeData((prevData) => ({ ...prevData, awards: current }));
|
|
}}
|
|
label="Icon"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Year</label>
|
|
<input
|
|
type="text"
|
|
value={award?.year || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
|
|
current[index] = { ...current[index], year: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, awards: current }));
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="2024"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Image</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
value={award?.image || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
|
|
current[index] = { ...current[index], image: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, awards: current }));
|
|
}}
|
|
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="URL or upload"
|
|
/>
|
|
<label className="px-3 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
|
<Upload className="w-3 h-3" />
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setHomeData((prevData) => {
|
|
const current = Array.isArray(prevData.awards) ? [...prevData.awards] : [];
|
|
current[index] = { ...current[index], image: imageUrl };
|
|
return { ...prevData, awards: current };
|
|
});
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{award?.image && (
|
|
<div className="mt-2">
|
|
<img src={award.image.startsWith('http') ? award.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${award.image}`} alt="Award preview" className="max-w-full max-h-32 rounded-lg border border-gray-200" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={award?.title || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
|
|
current[index] = { ...current[index], title: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, awards: current }));
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="Award Title"
|
|
/>
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Description</label>
|
|
<textarea
|
|
value={award?.description || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
|
|
current[index] = { ...current[index], description: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, awards: current }));
|
|
}}
|
|
rows={3}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="Award description"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{(!homeData.awards || homeData.awards.length === 0) && (
|
|
<p className="text-gray-500 text-center py-8">No awards added yet. Click "Add Award" to get started.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* CTA Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-2xl font-extrabold text-gray-900 mb-6">Call to Action Section</h2>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.cta_title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, cta_title: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Ready to Experience Luxury?"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.cta_subtitle || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, cta_subtitle: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Book your stay today"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Button Text</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.cta_button_text || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, cta_button_text: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Book Now"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Button Link</label>
|
|
<input
|
|
type="url"
|
|
value={homeData.cta_button_link || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, cta_button_link: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="/rooms"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">CTA Background Image</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
value={homeData.cta_image || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, cta_image: e.target.value })}
|
|
className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="https://example.com/cta-image.jpg or upload"
|
|
/>
|
|
<label className="px-4 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all cursor-pointer flex items-center gap-2">
|
|
<Upload className="w-4 h-4" />
|
|
Upload
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setHomeData((prevData) => ({ ...prevData, cta_image: imageUrl }));
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{homeData.cta_image && (
|
|
<div className="mt-2">
|
|
<img src={homeData.cta_image.startsWith('http') ? homeData.cta_image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${homeData.cta_image}`} alt="CTA preview" className="max-w-full max-h-48 rounded-lg border border-gray-200" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Partners Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-extrabold text-gray-900">Partners & Brands</h2>
|
|
</div>
|
|
<div className="mb-6 space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Title</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.partners_section_title || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, partners_section_title: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Our Partners"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Section Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={homeData.partners_section_subtitle || ''}
|
|
onChange={(e) => setHomeData({ ...homeData, partners_section_subtitle: e.target.value })}
|
|
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
|
|
placeholder="Trusted by leading brands"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const current = Array.isArray(homeData.partners) ? homeData.partners : [];
|
|
setHomeData((prevData) => ({
|
|
...prevData,
|
|
partners: [...current, { name: '', logo: '', link: '' }]
|
|
}));
|
|
}}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-lg font-semibold hover:from-amber-600 hover:to-amber-700 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Partner
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{Array.isArray(homeData.partners) && homeData.partners.map((partner, index) => (
|
|
<div key={`partner-${index}-${partner.name || index}`} className="p-6 border-2 border-gray-200 rounded-xl space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Partner {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const current = Array.isArray(homeData.partners) ? homeData.partners : [];
|
|
const updated = current.filter((_, i) => i !== index);
|
|
setHomeData((prevData) => ({ ...prevData, partners: updated }));
|
|
}}
|
|
className="text-red-600 hover:text-red-700 p-1"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Name</label>
|
|
<input
|
|
type="text"
|
|
value={partner?.name || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
|
|
current[index] = { ...current[index], name: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, partners: current }));
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="Partner Name"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Logo URL</label>
|
|
<input
|
|
type="url"
|
|
value={partner?.logo || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
|
|
current[index] = { ...current[index], logo: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, partners: current }));
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="https://example.com/logo.png"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Link (Optional)</label>
|
|
<input
|
|
type="url"
|
|
value={partner?.link || ''}
|
|
onChange={(e) => {
|
|
const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
|
|
current[index] = { ...current[index], link: e.target.value };
|
|
setHomeData((prevData) => ({ ...prevData, partners: current }));
|
|
}}
|
|
className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
|
|
placeholder="https://example.com"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{(!homeData.partners || homeData.partners.length === 0) && (
|
|
<p className="text-gray-500 text-center py-8">No partners added yet. Click "Add Partner" to get started.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Save Button */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={() => handleSave('home', homeData)}
|
|
disabled={saving}
|
|
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Save className="w-5 h-5" />
|
|
{saving ? 'Saving...' : 'Save All Home Content'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Banner Carousel Management Section */}
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h2 className="text-3xl font-extrabold text-gray-900 mb-2">Banner Carousel</h2>
|
|
<p className="text-gray-600">Manage homepage banner carousel images</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
resetBannerForm();
|
|
setShowBannerModal(true);
|
|
}}
|
|
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl"
|
|
>
|
|
<Plus className="w-5 h-5" />
|
|
<span>Add Banner</span>
|
|
</button>
|
|
</div>
|
|
|
|
{loadingBanners ? (
|
|
<div className="flex justify-center py-12">
|
|
<Loader2 className="w-8 h-8 animate-spin text-purple-500" />
|
|
</div>
|
|
) : banners.length === 0 ? (
|
|
<div className="text-center py-12 border-2 border-dashed border-gray-300 rounded-xl">
|
|
<ImageIcon className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
|
<p className="text-gray-600 font-medium mb-2">No banners found</p>
|
|
<p className="text-gray-500 text-sm">Add your first banner to get started</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Image</th>
|
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Title</th>
|
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Order</th>
|
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Status</th>
|
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{banners
|
|
.sort((a, b) => a.display_order - b.display_order)
|
|
.map((banner) => (
|
|
<tr key={banner.id} className="hover:bg-gray-50 transition-colors">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
{banner.image_url ? (
|
|
<img
|
|
src={banner.image_url}
|
|
alt={banner.title}
|
|
className="w-20 h-20 object-cover rounded-lg border border-gray-200"
|
|
/>
|
|
) : (
|
|
<div className="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center border border-gray-200">
|
|
<ImageIcon className="w-8 h-8 text-gray-400" />
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="text-sm font-semibold text-gray-900">{banner.title}</div>
|
|
{banner.description && (
|
|
<div className="text-xs text-gray-600 mt-1 line-clamp-2 max-w-xs">{banner.description}</div>
|
|
)}
|
|
{banner.link_url && (
|
|
<div className="text-xs text-gray-500 truncate max-w-xs mt-1">{banner.link_url}</div>
|
|
)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className="text-sm font-medium text-gray-900">{banner.display_order}</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<button
|
|
onClick={() => toggleBannerActive(banner)}
|
|
className={`px-3 py-1 rounded-full text-xs font-semibold transition-colors ${
|
|
banner.is_active
|
|
? 'bg-green-100 text-green-700 hover:bg-green-200'
|
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
{banner.is_active ? (
|
|
<span className="flex items-center gap-1">
|
|
<Check className="w-3 h-3" /> Active
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center gap-1">
|
|
<XCircle className="w-3 h-3" /> Inactive
|
|
</span>
|
|
)}
|
|
</button>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => handleEditBanner(banner)}
|
|
className="text-purple-600 hover:text-purple-700 font-semibold transition-colors"
|
|
>
|
|
<Edit className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => setDeleteConfirm({ show: true, id: banner.id })}
|
|
className="text-red-600 hover:text-red-700 font-semibold transition-colors"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Banner Modal */}
|
|
{showBannerModal && (
|
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
|
<div className="fixed inset-0 bg-black/70 backdrop-blur-md" onClick={() => setShowBannerModal(false)}></div>
|
|
<div className="flex min-h-full items-center justify-center p-3 sm:p-4">
|
|
<div className="relative bg-white rounded-2xl sm:rounded-3xl shadow-2xl max-w-2xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden border border-slate-200/50 animate-scale-in">
|
|
<div className="sticky top-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 px-4 sm:px-6 py-4 sm:py-5 border-b border-slate-700 z-10">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h3 className="text-lg sm:text-xl md:text-2xl font-bold text-amber-100">
|
|
{editingBanner ? 'Edit Banner' : 'Add Banner'}
|
|
</h3>
|
|
<p className="text-amber-200/80 text-xs sm:text-sm font-light mt-1">
|
|
{editingBanner ? 'Modify banner information' : 'Create a new promotional banner'}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
setShowBannerModal(false);
|
|
resetBannerForm();
|
|
}}
|
|
className="w-9 h-9 sm:w-10 sm:h-10 flex items-center justify-center rounded-xl text-amber-100 hover:text-white hover:bg-slate-700/50 transition-all duration-200 border border-slate-600 hover:border-amber-400"
|
|
>
|
|
<X className="w-5 h-5 sm:w-6 sm:h-6" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-y-auto max-h-[calc(95vh-120px)] sm:max-h-[calc(90vh-120px)]">
|
|
<form onSubmit={handleBannerSubmit} className="p-4 sm:p-6 space-y-5 sm:space-y-6">
|
|
<div>
|
|
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">Title *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={bannerFormData.title}
|
|
onChange={(e) => setBannerFormData({ ...bannerFormData, title: e.target.value })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-slate-700 font-medium shadow-sm"
|
|
placeholder="Banner title"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">Description (Optional)</label>
|
|
<textarea
|
|
value={bannerFormData.description}
|
|
onChange={(e) => setBannerFormData({ ...bannerFormData, description: e.target.value })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 min-h-[100px] resize-y text-slate-700 font-medium shadow-sm"
|
|
placeholder="Banner description text that appears below the title"
|
|
rows={3}
|
|
/>
|
|
<p className="text-xs text-slate-500 mt-1">This description will appear below the title on the banner</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">Link (Optional)</label>
|
|
<input
|
|
type="url"
|
|
value={bannerFormData.link_url}
|
|
onChange={(e) => setBannerFormData({ ...bannerFormData, link_url: e.target.value })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-slate-700 font-medium shadow-sm"
|
|
placeholder="https://example.com"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">Display Order</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={bannerFormData.display_order}
|
|
onChange={(e) => setBannerFormData({ ...bannerFormData, display_order: parseInt(e.target.value) || 0 })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-slate-700 font-medium shadow-sm"
|
|
placeholder="0"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">Start Date (Optional)</label>
|
|
<input
|
|
type="date"
|
|
value={bannerFormData.start_date}
|
|
onChange={(e) => setBannerFormData({ ...bannerFormData, start_date: e.target.value })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-slate-700 font-medium shadow-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">End Date (Optional)</label>
|
|
<input
|
|
type="date"
|
|
value={bannerFormData.end_date}
|
|
onChange={(e) => setBannerFormData({ ...bannerFormData, end_date: e.target.value })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-slate-700 font-medium shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mb-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => setUseFileUpload(true)}
|
|
className={`px-4 py-2.5 rounded-xl text-sm font-semibold transition-all duration-200 ${
|
|
useFileUpload
|
|
? 'bg-gradient-to-r from-amber-500 to-amber-600 text-white shadow-lg shadow-amber-500/30'
|
|
: 'bg-slate-100 text-slate-700 hover:bg-slate-200 border-2 border-slate-200'
|
|
}`}
|
|
>
|
|
Upload File
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setUseFileUpload(false)}
|
|
className={`px-4 py-2.5 rounded-xl text-sm font-semibold transition-all duration-200 ${
|
|
!useFileUpload
|
|
? 'bg-gradient-to-r from-amber-500 to-amber-600 text-white shadow-lg shadow-amber-500/30'
|
|
: 'bg-slate-100 text-slate-700 hover:bg-slate-200 border-2 border-slate-200'
|
|
}`}
|
|
>
|
|
Image URL
|
|
</button>
|
|
</div>
|
|
|
|
{useFileUpload ? (
|
|
<div>
|
|
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">Banner Image *</label>
|
|
<div className="border-2 border-dashed border-slate-300 rounded-xl p-4 sm:p-6 text-center hover:border-amber-400 transition-all duration-200 bg-gradient-to-br from-slate-50 to-white">
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleImageChange}
|
|
className="hidden"
|
|
id="banner-image-upload"
|
|
/>
|
|
<label
|
|
htmlFor="banner-image-upload"
|
|
className="cursor-pointer flex flex-col items-center"
|
|
>
|
|
{imagePreview ? (
|
|
<img
|
|
src={imagePreview}
|
|
alt="Preview"
|
|
className="max-w-full max-h-48 sm:max-h-64 rounded-lg mb-4 border-2 border-slate-200"
|
|
/>
|
|
) : (
|
|
<>
|
|
<Upload className="w-10 h-10 sm:w-12 sm:h-12 text-slate-400 mb-3" />
|
|
<span className="text-slate-600 font-medium text-sm sm:text-base">Click to upload image</span>
|
|
<span className="text-slate-500 text-xs sm:text-sm mt-1">PNG, JPG up to 5MB</span>
|
|
</>
|
|
)}
|
|
</label>
|
|
{uploadingImage && (
|
|
<div className="mt-4 flex items-center justify-center gap-2 text-amber-600">
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
<span className="text-sm font-medium">Uploading...</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<label className="block text-xs sm:text-sm font-semibold text-slate-600 uppercase tracking-wider mb-2">Image URL *</label>
|
|
<input
|
|
type="url"
|
|
required
|
|
value={bannerFormData.image_url}
|
|
onChange={(e) => {
|
|
setBannerFormData({ ...bannerFormData, image_url: e.target.value });
|
|
setImagePreview(e.target.value);
|
|
}}
|
|
className="w-full px-4 py-3 bg-white border-2 border-slate-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200 text-slate-700 font-medium shadow-sm"
|
|
placeholder="https://example.com/image.jpg"
|
|
/>
|
|
{imagePreview && (
|
|
<img
|
|
src={imagePreview}
|
|
alt="Preview"
|
|
className="mt-4 max-w-full max-h-48 sm:max-h-64 rounded-lg border-2 border-slate-200"
|
|
onError={() => setImagePreview(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row items-center gap-3 pt-4 border-t border-slate-200">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowBannerModal(false);
|
|
resetBannerForm();
|
|
}}
|
|
className="w-full sm:flex-1 px-6 py-3 border-2 border-slate-300 text-slate-700 rounded-xl font-semibold hover:bg-slate-50 transition-all duration-200 shadow-sm hover:shadow-md"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={uploadingImage}
|
|
className="w-full sm:flex-1 px-6 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
>
|
|
{uploadingImage ? (
|
|
<>
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
Uploading...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="w-5 h-5" />
|
|
{editingBanner ? 'Update Banner' : 'Add Banner'}
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<ConfirmationDialog
|
|
isOpen={deleteConfirm.show}
|
|
onClose={() => setDeleteConfirm({ show: false, id: null })}
|
|
onConfirm={handleDeleteBanner}
|
|
title="Delete Banner"
|
|
message="Are you sure you want to delete this banner? This action cannot be undone."
|
|
confirmText="Delete"
|
|
cancelText="Cancel"
|
|
variant="danger"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Contact Tab */}
|
|
{activeTab === 'contact' && (
|
|
<div className="space-y-8">
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-3xl font-extrabold text-gray-900 mb-6">Contact Page Content</h2>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Page Title</label>
|
|
<input
|
|
type="text"
|
|
value={contactData.title || ''}
|
|
onChange={(e) => setContactData({ ...contactData, title: 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">Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={contactData.subtitle || ''}
|
|
onChange={(e) => setContactData({ ...contactData, subtitle: 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">Description</label>
|
|
<textarea
|
|
value={contactData.description || ''}
|
|
onChange={(e) => setContactData({ ...contactData, description: e.target.value })}
|
|
rows={4}
|
|
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="border-t border-gray-200 pt-6">
|
|
<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>
|
|
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<h3 className="text-xl font-bold text-gray-900 mb-4">Google Maps</h3>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Map Embed URL</label>
|
|
<input
|
|
type="url"
|
|
value={contactData.map_url || ''}
|
|
onChange={(e) => setContactData({ ...contactData, map_url: 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"
|
|
placeholder="https://www.google.com/maps/embed?pb=..."
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
Paste the Google Maps embed URL here. You can get this by clicking "Share" → "Embed a map" on Google Maps.
|
|
</p>
|
|
{contactData.map_url && (
|
|
<div className="mt-4 border-2 border-gray-200 rounded-xl overflow-hidden">
|
|
<iframe
|
|
src={contactData.map_url}
|
|
width="100%"
|
|
height="300"
|
|
style={{ border: 0 }}
|
|
allowFullScreen
|
|
loading="lazy"
|
|
referrerPolicy="no-referrer-when-downgrade"
|
|
title="Map Preview"
|
|
/>
|
|
</div>
|
|
)}
|
|
</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>
|
|
<input
|
|
type="text"
|
|
value={contactData.meta_title || ''}
|
|
onChange={(e) => setContactData({ ...contactData, meta_title: 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">Meta Description</label>
|
|
<textarea
|
|
value={contactData.meta_description || ''}
|
|
onChange={(e) => setContactData({ ...contactData, meta_description: 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={() => handleSave('contact', contactData)}
|
|
disabled={saving}
|
|
className="px-8 py-3 bg-gradient-to-r from-green-500 to-green-600 text-white rounded-xl font-semibold hover:from-green-600 hover:to-green-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Save className="w-5 h-5" />
|
|
{saving ? 'Saving...' : 'Save Contact Content'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* About Tab */}
|
|
{activeTab === 'about' && (
|
|
<div className="space-y-8">
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-3xl font-extrabold text-gray-900 mb-6">About Page Content</h2>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Page Title</label>
|
|
<input
|
|
type="text"
|
|
value={aboutData.title || ''}
|
|
onChange={(e) => setAboutData({ ...aboutData, title: 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">Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={aboutData.subtitle || ''}
|
|
onChange={(e) => setAboutData({ ...aboutData, subtitle: 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">Our Story</label>
|
|
<textarea
|
|
value={aboutData.story_content || ''}
|
|
onChange={(e) => setAboutData({ ...aboutData, story_content: e.target.value })}
|
|
rows={6}
|
|
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="Tell your story..."
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Description</label>
|
|
<textarea
|
|
value={aboutData.description || ''}
|
|
onChange={(e) => setAboutData({ ...aboutData, description: e.target.value })}
|
|
rows={4}
|
|
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>
|
|
|
|
{/* Hero Image */}
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Hero Image</label>
|
|
<div className="flex gap-4">
|
|
<input
|
|
type="text"
|
|
value={aboutData.about_hero_image || ''}
|
|
onChange={(e) => setAboutData({ ...aboutData, about_hero_image: e.target.value })}
|
|
className="flex-1 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="https://example.com/hero-image.jpg or upload"
|
|
/>
|
|
<label className="px-4 py-3 bg-purple-600 text-white rounded-xl hover:bg-purple-700 transition-colors flex items-center gap-2 cursor-pointer">
|
|
<Upload className="w-5 h-5" />
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setAboutData((prevData) => ({ ...prevData, about_hero_image: imageUrl }));
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
Upload
|
|
</label>
|
|
</div>
|
|
{aboutData.about_hero_image && (
|
|
<div className="mt-4">
|
|
<img src={aboutData.about_hero_image.startsWith('http') ? aboutData.about_hero_image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${aboutData.about_hero_image}`} alt="Hero" className="max-w-full max-h-48 rounded-lg border border-gray-200" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Mission */}
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Mission Statement</label>
|
|
<textarea
|
|
value={aboutData.mission || ''}
|
|
onChange={(e) => setAboutData({ ...aboutData, mission: e.target.value })}
|
|
rows={4}
|
|
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 mission is to..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Vision */}
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Vision Statement</label>
|
|
<textarea
|
|
value={aboutData.vision || ''}
|
|
onChange={(e) => setAboutData({ ...aboutData, vision: e.target.value })}
|
|
rows={4}
|
|
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 vision is to..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Values Section */}
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-xl font-bold text-gray-900">Our Values</h3>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const newValue = { icon: 'Heart', title: '', description: '' };
|
|
setAboutData((prevData) => ({
|
|
...prevData,
|
|
values: [...(Array.isArray(prevData.values) ? prevData.values : []), newValue]
|
|
}));
|
|
}}
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex items-center gap-2"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Value
|
|
</button>
|
|
</div>
|
|
{Array.isArray(aboutData.values) && aboutData.values.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{aboutData.values.map((value, index) => (
|
|
<div key={`value-${index}`} className="p-4 border-2 border-gray-200 rounded-xl space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Value {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setAboutData((prevData) => ({
|
|
...prevData,
|
|
values: (prevData.values || []).filter((_, i) => i !== index)
|
|
}));
|
|
}}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
|
<IconPicker
|
|
value={value.icon || 'Heart'}
|
|
onChange={(icon: string) => {
|
|
setAboutData((prevData) => {
|
|
const newValues = [...(prevData.values || [])];
|
|
newValues[index] = { ...newValues[index], icon };
|
|
return { ...prevData, values: newValues };
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Title</label>
|
|
<input
|
|
type="text"
|
|
value={value.title || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newValues = [...(prevData.values || [])];
|
|
newValues[index] = { ...newValues[index], title: e.target.value };
|
|
return { ...prevData, values: newValues };
|
|
});
|
|
}}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="Value title"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
<textarea
|
|
value={value.description || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newValues = [...(prevData.values || [])];
|
|
newValues[index] = { ...newValues[index], description: e.target.value };
|
|
return { ...prevData, values: newValues };
|
|
});
|
|
}}
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="Value description"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500 text-sm">No values added yet. Click "Add Value" to get started.</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Features Section */}
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-xl font-bold text-gray-900">Features</h3>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const newFeature = { icon: 'Star', title: '', description: '', image: '' };
|
|
setAboutData((prevData) => ({
|
|
...prevData,
|
|
features: [...(Array.isArray(prevData.features) ? prevData.features : []), newFeature]
|
|
}));
|
|
}}
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex items-center gap-2"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Feature
|
|
</button>
|
|
</div>
|
|
{Array.isArray(aboutData.features) && aboutData.features.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{aboutData.features.map((feature, index) => (
|
|
<div key={`feature-${index}`} className="p-4 border-2 border-gray-200 rounded-xl space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Feature {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setAboutData((prevData) => ({
|
|
...prevData,
|
|
features: (prevData.features || []).filter((_, i) => i !== index)
|
|
}));
|
|
}}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
|
<IconPicker
|
|
value={feature.icon || 'Star'}
|
|
onChange={(icon: string) => {
|
|
setAboutData((prevData) => {
|
|
const newFeatures = [...(prevData.features || [])];
|
|
newFeatures[index] = { ...newFeatures[index], icon };
|
|
return { ...prevData, features: newFeatures };
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Title</label>
|
|
<input
|
|
type="text"
|
|
value={feature.title || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newFeatures = [...(prevData.features || [])];
|
|
newFeatures[index] = { ...newFeatures[index], title: e.target.value };
|
|
return { ...prevData, features: newFeatures };
|
|
});
|
|
}}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="Feature title"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
<textarea
|
|
value={feature.description || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newFeatures = [...(prevData.features || [])];
|
|
newFeatures[index] = { ...newFeatures[index], description: e.target.value };
|
|
return { ...prevData, features: newFeatures };
|
|
});
|
|
}}
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="Feature description"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500 text-sm">No features added yet. Click "Add Feature" to get started.</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Team Section */}
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-xl font-bold text-gray-900">Team Members</h3>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const newMember = { name: '', role: '', image: '', bio: '', social_links: {} };
|
|
setAboutData((prevData) => ({
|
|
...prevData,
|
|
team: [...(Array.isArray(prevData.team) ? prevData.team : []), newMember]
|
|
}));
|
|
}}
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex items-center gap-2"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Team Member
|
|
</button>
|
|
</div>
|
|
{Array.isArray(aboutData.team) && aboutData.team.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{aboutData.team.map((member, index) => (
|
|
<div key={`team-${index}`} className="p-4 border-2 border-gray-200 rounded-xl space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Team Member {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setAboutData((prevData) => ({
|
|
...prevData,
|
|
team: (prevData.team || []).filter((_, i) => i !== index)
|
|
}));
|
|
}}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
<input
|
|
type="text"
|
|
value={member.name || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newTeam = [...(prevData.team || [])];
|
|
newTeam[index] = { ...newTeam[index], name: e.target.value };
|
|
return { ...prevData, team: newTeam };
|
|
});
|
|
}}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="Full name"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
|
<input
|
|
type="text"
|
|
value={member.role || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newTeam = [...(prevData.team || [])];
|
|
newTeam[index] = { ...newTeam[index], role: e.target.value };
|
|
return { ...prevData, team: newTeam };
|
|
});
|
|
}}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="Job title"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Image URL</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={member.image || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newTeam = [...(prevData.team || [])];
|
|
newTeam[index] = { ...newTeam[index], image: e.target.value };
|
|
return { ...prevData, team: newTeam };
|
|
});
|
|
}}
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="https://example.com/image.jpg or upload"
|
|
/>
|
|
<label className="px-3 py-2 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-lg font-semibold hover:from-purple-600 hover:to-purple-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
|
<Upload className="w-3 h-3" />
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setAboutData((prevData) => {
|
|
const newTeam = [...(prevData.team || [])];
|
|
newTeam[index] = { ...newTeam[index], image: imageUrl };
|
|
return { ...prevData, team: newTeam };
|
|
});
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{member.image && (
|
|
<div className="mt-2">
|
|
<img
|
|
src={member.image.startsWith('http') ? member.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${member.image}`}
|
|
alt="Team member preview"
|
|
className="max-w-full max-h-32 rounded-lg border border-gray-200"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Bio</label>
|
|
<textarea
|
|
value={member.bio || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newTeam = [...(prevData.team || [])];
|
|
newTeam[index] = { ...newTeam[index], bio: e.target.value };
|
|
return { ...prevData, team: newTeam };
|
|
});
|
|
}}
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="Short biography"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500 text-sm">No team members added yet. Click "Add Team Member" to get started.</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Timeline Section */}
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-xl font-bold text-gray-900">Timeline / History</h3>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const newEvent = { year: '', title: '', description: '', image: '' };
|
|
setAboutData((prevData) => ({
|
|
...prevData,
|
|
timeline: [...(Array.isArray(prevData.timeline) ? prevData.timeline : []), newEvent]
|
|
}));
|
|
}}
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex items-center gap-2"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Event
|
|
</button>
|
|
</div>
|
|
{Array.isArray(aboutData.timeline) && aboutData.timeline.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{aboutData.timeline.map((event, index) => (
|
|
<div key={`timeline-${index}`} className="p-4 border-2 border-gray-200 rounded-xl space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Event {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setAboutData((prevData) => ({
|
|
...prevData,
|
|
timeline: (prevData.timeline || []).filter((_, i) => i !== index)
|
|
}));
|
|
}}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Year</label>
|
|
<input
|
|
type="text"
|
|
value={event.year || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newTimeline = [...(prevData.timeline || [])];
|
|
newTimeline[index] = { ...newTimeline[index], year: e.target.value };
|
|
return { ...prevData, timeline: newTimeline };
|
|
});
|
|
}}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="2020"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Title</label>
|
|
<input
|
|
type="text"
|
|
value={event.title || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newTimeline = [...(prevData.timeline || [])];
|
|
newTimeline[index] = { ...newTimeline[index], title: e.target.value };
|
|
return { ...prevData, timeline: newTimeline };
|
|
});
|
|
}}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="Event title"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
<textarea
|
|
value={event.description || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newTimeline = [...(prevData.timeline || [])];
|
|
newTimeline[index] = { ...newTimeline[index], description: e.target.value };
|
|
return { ...prevData, timeline: newTimeline };
|
|
});
|
|
}}
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="Event description"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Image URL</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={event.image || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newTimeline = [...(prevData.timeline || [])];
|
|
newTimeline[index] = { ...newTimeline[index], image: e.target.value };
|
|
return { ...prevData, timeline: newTimeline };
|
|
});
|
|
}}
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="https://example.com/image.jpg or upload"
|
|
/>
|
|
<label className="px-3 py-2 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-lg font-semibold hover:from-purple-600 hover:to-purple-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
|
<Upload className="w-3 h-3" />
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setAboutData((prevData) => {
|
|
const newTimeline = [...(prevData.timeline || [])];
|
|
newTimeline[index] = { ...newTimeline[index], image: imageUrl };
|
|
return { ...prevData, timeline: newTimeline };
|
|
});
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{event.image && (
|
|
<div className="mt-2">
|
|
<img
|
|
src={event.image.startsWith('http') ? event.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${event.image}`}
|
|
alt="Timeline event preview"
|
|
className="max-w-full max-h-32 rounded-lg border border-gray-200"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500 text-sm">No timeline events added yet. Click "Add Event" to get started.</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Achievements Section */}
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-xl font-bold text-gray-900">Achievements & Awards</h3>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const newAchievement = { icon: 'Award', title: '', description: '', year: '', image: '' };
|
|
setAboutData((prevData) => ({
|
|
...prevData,
|
|
achievements: [...(Array.isArray(prevData.achievements) ? prevData.achievements : []), newAchievement]
|
|
}));
|
|
}}
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex items-center gap-2"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Achievement
|
|
</button>
|
|
</div>
|
|
{Array.isArray(aboutData.achievements) && aboutData.achievements.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{aboutData.achievements.map((achievement, index) => (
|
|
<div key={`achievement-${index}`} className="p-4 border-2 border-gray-200 rounded-xl space-y-3">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold text-gray-900">Achievement {index + 1}</h4>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setAboutData((prevData) => ({
|
|
...prevData,
|
|
achievements: (prevData.achievements || []).filter((_, i) => i !== index)
|
|
}));
|
|
}}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Icon</label>
|
|
<IconPicker
|
|
value={achievement.icon || 'Award'}
|
|
onChange={(icon: string) => {
|
|
setAboutData((prevData) => {
|
|
const newAchievements = [...(prevData.achievements || [])];
|
|
newAchievements[index] = { ...newAchievements[index], icon };
|
|
return { ...prevData, achievements: newAchievements };
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Title</label>
|
|
<input
|
|
type="text"
|
|
value={achievement.title || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newAchievements = [...(prevData.achievements || [])];
|
|
newAchievements[index] = { ...newAchievements[index], title: e.target.value };
|
|
return { ...prevData, achievements: newAchievements };
|
|
});
|
|
}}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="Achievement title"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Year</label>
|
|
<input
|
|
type="text"
|
|
value={achievement.year || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newAchievements = [...(prevData.achievements || [])];
|
|
newAchievements[index] = { ...newAchievements[index], year: e.target.value };
|
|
return { ...prevData, achievements: newAchievements };
|
|
});
|
|
}}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="2020"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
<textarea
|
|
value={achievement.description || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newAchievements = [...(prevData.achievements || [])];
|
|
newAchievements[index] = { ...newAchievements[index], description: e.target.value };
|
|
return { ...prevData, achievements: newAchievements };
|
|
});
|
|
}}
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="Achievement description"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Image URL</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={achievement.image || ''}
|
|
onChange={(e) => {
|
|
setAboutData((prevData) => {
|
|
const newAchievements = [...(prevData.achievements || [])];
|
|
newAchievements[index] = { ...newAchievements[index], image: e.target.value };
|
|
return { ...prevData, achievements: newAchievements };
|
|
});
|
|
}}
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="https://example.com/image.jpg or upload"
|
|
/>
|
|
<label className="px-3 py-2 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-lg font-semibold hover:from-purple-600 hover:to-purple-700 transition-all cursor-pointer flex items-center gap-1 text-sm">
|
|
<Upload className="w-3 h-3" />
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
await handlePageContentImageUpload(file, (imageUrl) => {
|
|
setAboutData((prevData) => {
|
|
const newAchievements = [...(prevData.achievements || [])];
|
|
newAchievements[index] = { ...newAchievements[index], image: imageUrl };
|
|
return { ...prevData, achievements: newAchievements };
|
|
});
|
|
});
|
|
}
|
|
}}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</div>
|
|
{achievement.image && (
|
|
<div className="mt-2">
|
|
<img
|
|
src={achievement.image.startsWith('http') ? achievement.image : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${achievement.image}`}
|
|
alt="Achievement preview"
|
|
className="max-w-full max-h-32 rounded-lg border border-gray-200"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500 text-sm">No achievements added yet. Click "Add Achievement" to get started.</p>
|
|
)}
|
|
</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>
|
|
<input
|
|
type="text"
|
|
value={aboutData.meta_title || ''}
|
|
onChange={(e) => setAboutData({ ...aboutData, meta_title: 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">Meta Description</label>
|
|
<textarea
|
|
value={aboutData.meta_description || ''}
|
|
onChange={(e) => setAboutData({ ...aboutData, meta_description: 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={() => handleSave('about', aboutData)}
|
|
disabled={saving}
|
|
className="px-8 py-3 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-semibold hover:from-amber-600 hover:to-amber-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Save className="w-5 h-5" />
|
|
{saving ? 'Saving...' : 'Save About Content'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Footer Tab */}
|
|
{activeTab === 'footer' && (
|
|
<div className="space-y-8">
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-3xl font-extrabold text-gray-900 mb-6">Footer Content</h2>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Company Name</label>
|
|
<input
|
|
type="text"
|
|
value={footerData.title || ''}
|
|
onChange={(e) => setFooterData({ ...footerData, title: 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">Description</label>
|
|
<textarea
|
|
value={footerData.description || ''}
|
|
onChange={(e) => setFooterData({ ...footerData, description: e.target.value })}
|
|
rows={4}
|
|
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="border-t border-gray-200 pt-6">
|
|
<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>
|
|
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<h3 className="text-xl font-bold text-gray-900 mb-4">Social Media Links</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Facebook</label>
|
|
<input
|
|
type="url"
|
|
value={footerData.social_links?.facebook || ''}
|
|
onChange={(e) => setFooterData({
|
|
...footerData,
|
|
social_links: { ...footerData.social_links, facebook: 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"
|
|
placeholder="https://facebook.com/..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Twitter</label>
|
|
<input
|
|
type="url"
|
|
value={footerData.social_links?.twitter || ''}
|
|
onChange={(e) => setFooterData({
|
|
...footerData,
|
|
social_links: { ...footerData.social_links, twitter: 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"
|
|
placeholder="https://twitter.com/..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Instagram</label>
|
|
<input
|
|
type="url"
|
|
value={footerData.social_links?.instagram || ''}
|
|
onChange={(e) => setFooterData({
|
|
...footerData,
|
|
social_links: { ...footerData.social_links, instagram: 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"
|
|
placeholder="https://instagram.com/..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">LinkedIn</label>
|
|
<input
|
|
type="url"
|
|
value={footerData.social_links?.linkedin || ''}
|
|
onChange={(e) => setFooterData({
|
|
...footerData,
|
|
social_links: { ...footerData.social_links, linkedin: 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"
|
|
placeholder="https://linkedin.com/..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">YouTube</label>
|
|
<input
|
|
type="url"
|
|
value={footerData.social_links?.youtube || ''}
|
|
onChange={(e) => setFooterData({
|
|
...footerData,
|
|
social_links: { ...footerData.social_links, youtube: 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"
|
|
placeholder="https://youtube.com/..."
|
|
/>
|
|
</div>
|
|
</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>
|
|
|
|
{/* Copyright Text */}
|
|
<div className="bg-gradient-to-br from-amber-50 to-yellow-50 rounded-xl p-6 border-2 border-amber-200">
|
|
<h3 className="text-lg font-bold text-gray-900 mb-4">Copyright Text</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
|
Copyright Text <span className="text-amber-600">(Use {`{YEAR}`} for automatic year)</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={footerData.copyright_text || ''}
|
|
onChange={(e) => setFooterData({ ...footerData, copyright_text: e.target.value })}
|
|
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200"
|
|
placeholder="© {YEAR} Luxury Hotel. All rights reserved."
|
|
/>
|
|
<p className="mt-2 text-xs text-gray-600">
|
|
The {`{YEAR}`} placeholder will be automatically replaced with the current year.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={() => handleSave('footer', footerData)}
|
|
disabled={saving}
|
|
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Save className="w-5 h-5" />
|
|
{saving ? 'Saving...' : 'Save Footer Content'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Privacy Tab */}
|
|
{activeTab === 'privacy' && (
|
|
<div className="space-y-8">
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-3xl font-extrabold text-gray-900">Privacy Policy Content</h2>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm font-medium text-gray-700">Enable Page</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setPrivacyData({ ...privacyData, is_active: !privacyData.is_active })}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
|
(privacyData.is_active ?? true) ? 'bg-purple-600' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
(privacyData.is_active ?? true) ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={privacyData.title || ''}
|
|
onChange={(e) => setPrivacyData({ ...privacyData, title: 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"
|
|
placeholder="Privacy Policy"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={privacyData.subtitle || ''}
|
|
onChange={(e) => setPrivacyData({ ...privacyData, subtitle: 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"
|
|
placeholder="Your privacy is important to us"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
|
<textarea
|
|
value={privacyData.content || ''}
|
|
onChange={(e) => setPrivacyData({ ...privacyData, content: e.target.value })}
|
|
rows={20}
|
|
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 font-mono text-sm"
|
|
placeholder="Enter HTML content for privacy policy..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<h3 className="text-xl font-bold text-gray-900 mb-4">SEO Settings</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
|
<input
|
|
type="text"
|
|
value={privacyData.meta_title || ''}
|
|
onChange={(e) => setPrivacyData({ ...privacyData, meta_title: 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">Meta Description</label>
|
|
<textarea
|
|
value={privacyData.meta_description || ''}
|
|
onChange={(e) => setPrivacyData({ ...privacyData, meta_description: 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={() => handleSave('privacy', privacyData)}
|
|
disabled={saving}
|
|
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Save className="w-5 h-5" />
|
|
{saving ? 'Saving...' : 'Save Privacy Policy'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Terms Tab */}
|
|
{activeTab === 'terms' && (
|
|
<div className="space-y-8">
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-3xl font-extrabold text-gray-900 mb-6">Terms & Conditions Content</h2>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={termsData.title || ''}
|
|
onChange={(e) => setTermsData({ ...termsData, title: 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"
|
|
placeholder="Terms & Conditions"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={termsData.subtitle || ''}
|
|
onChange={(e) => setTermsData({ ...termsData, subtitle: 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"
|
|
placeholder="Please read these terms carefully"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
|
<textarea
|
|
value={termsData.content || ''}
|
|
onChange={(e) => setTermsData({ ...termsData, content: e.target.value })}
|
|
rows={20}
|
|
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 font-mono text-sm"
|
|
placeholder="Enter HTML content for terms & conditions..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<h3 className="text-xl font-bold text-gray-900 mb-4">SEO Settings</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
|
<input
|
|
type="text"
|
|
value={termsData.meta_title || ''}
|
|
onChange={(e) => setTermsData({ ...termsData, meta_title: 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">Meta Description</label>
|
|
<textarea
|
|
value={termsData.meta_description || ''}
|
|
onChange={(e) => setTermsData({ ...termsData, meta_description: 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={() => handleSave('terms', termsData)}
|
|
disabled={saving}
|
|
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Save className="w-5 h-5" />
|
|
{saving ? 'Saving...' : 'Save Terms & Conditions'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Refunds Tab */}
|
|
{activeTab === 'refunds' && (
|
|
<div className="space-y-8">
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-3xl font-extrabold text-gray-900">Refunds Policy Content</h2>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm font-medium text-gray-700">Enable Page</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setRefundsData({ ...refundsData, is_active: !refundsData.is_active })}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
|
(refundsData.is_active ?? true) ? 'bg-purple-600' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
(refundsData.is_active ?? true) ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={refundsData.title || ''}
|
|
onChange={(e) => setRefundsData({ ...refundsData, title: 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"
|
|
placeholder="Refunds Policy"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={refundsData.subtitle || ''}
|
|
onChange={(e) => setRefundsData({ ...refundsData, subtitle: 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"
|
|
placeholder="Our commitment to fair refunds"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
|
<textarea
|
|
value={refundsData.content || ''}
|
|
onChange={(e) => setRefundsData({ ...refundsData, content: e.target.value })}
|
|
rows={20}
|
|
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 font-mono text-sm"
|
|
placeholder="Enter HTML content for refunds policy..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<h3 className="text-xl font-bold text-gray-900 mb-4">SEO Settings</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
|
<input
|
|
type="text"
|
|
value={refundsData.meta_title || ''}
|
|
onChange={(e) => setRefundsData({ ...refundsData, meta_title: 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">Meta Description</label>
|
|
<textarea
|
|
value={refundsData.meta_description || ''}
|
|
onChange={(e) => setRefundsData({ ...refundsData, meta_description: 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={() => handleSave('refunds', refundsData)}
|
|
disabled={saving}
|
|
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Save className="w-5 h-5" />
|
|
{saving ? 'Saving...' : 'Save Refunds Policy'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Cancellation Tab */}
|
|
{activeTab === 'cancellation' && (
|
|
<div className="space-y-8">
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-3xl font-extrabold text-gray-900">Cancellation Policy Content</h2>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm font-medium text-gray-700">Enable Page</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setCancellationData({ ...cancellationData, is_active: !cancellationData.is_active })}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
|
(cancellationData.is_active ?? true) ? 'bg-purple-600' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
(cancellationData.is_active ?? true) ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={cancellationData.title || ''}
|
|
onChange={(e) => setCancellationData({ ...cancellationData, title: 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"
|
|
placeholder="Cancellation Policy"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={cancellationData.subtitle || ''}
|
|
onChange={(e) => setCancellationData({ ...cancellationData, subtitle: 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"
|
|
placeholder="Flexible cancellation options for your peace of mind"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
|
<textarea
|
|
value={cancellationData.content || ''}
|
|
onChange={(e) => setCancellationData({ ...cancellationData, content: e.target.value })}
|
|
rows={20}
|
|
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 font-mono text-sm"
|
|
placeholder="Enter HTML content for cancellation policy..."
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
|
<input
|
|
type="text"
|
|
value={cancellationData.meta_title || ''}
|
|
onChange={(e) => setCancellationData({ ...cancellationData, meta_title: 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"
|
|
placeholder="Cancellation Policy - Luxury Hotel"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Description</label>
|
|
<textarea
|
|
value={cancellationData.meta_description || ''}
|
|
onChange={(e) => setCancellationData({ ...cancellationData, meta_description: 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="Review our cancellation policy..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 pt-6 border-t border-gray-200">
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={() => handleSave('cancellation', cancellationData)}
|
|
disabled={saving}
|
|
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Save className="w-5 h-5" />
|
|
{saving ? 'Saving...' : 'Save Cancellation Policy'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Accessibility Tab */}
|
|
{activeTab === 'accessibility' && (
|
|
<div className="space-y-8">
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-3xl font-extrabold text-gray-900">Accessibility Content</h2>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm font-medium text-gray-700">Enable Page</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setAccessibilityData({ ...accessibilityData, is_active: !accessibilityData.is_active })}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
|
(accessibilityData.is_active ?? true) ? 'bg-purple-600' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
(accessibilityData.is_active ?? true) ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={accessibilityData.title || ''}
|
|
onChange={(e) => setAccessibilityData({ ...accessibilityData, title: 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"
|
|
placeholder="Accessibility"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={accessibilityData.subtitle || ''}
|
|
onChange={(e) => setAccessibilityData({ ...accessibilityData, subtitle: 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"
|
|
placeholder="Committed to providing an inclusive experience for all guests"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
|
<textarea
|
|
value={accessibilityData.content || ''}
|
|
onChange={(e) => setAccessibilityData({ ...accessibilityData, content: e.target.value })}
|
|
rows={20}
|
|
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 font-mono text-sm"
|
|
placeholder="Enter HTML content for accessibility page..."
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
|
<input
|
|
type="text"
|
|
value={accessibilityData.meta_title || ''}
|
|
onChange={(e) => setAccessibilityData({ ...accessibilityData, meta_title: 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"
|
|
placeholder="Accessibility - Luxury Hotel"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Description</label>
|
|
<textarea
|
|
value={accessibilityData.meta_description || ''}
|
|
onChange={(e) => setAccessibilityData({ ...accessibilityData, meta_description: 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="Discover our commitment to accessibility..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 pt-6 border-t border-gray-200">
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={() => handleSave('accessibility', accessibilityData)}
|
|
disabled={saving}
|
|
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Save className="w-5 h-5" />
|
|
{saving ? 'Saving...' : 'Save Accessibility Content'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* FAQ Tab */}
|
|
{activeTab === 'faq' && (
|
|
<div className="space-y-8">
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-3xl font-extrabold text-gray-900">FAQ Content</h2>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm font-medium text-gray-700">Enable Page</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setFaqData({ ...faqData, is_active: !faqData.is_active })}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
|
(faqData.is_active ?? true) ? 'bg-purple-600' : 'bg-gray-300'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
(faqData.is_active ?? true) ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
|
<input
|
|
type="text"
|
|
value={faqData.title || ''}
|
|
onChange={(e) => setFaqData({ ...faqData, title: 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"
|
|
placeholder="Frequently Asked Questions"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
|
<input
|
|
type="text"
|
|
value={faqData.subtitle || ''}
|
|
onChange={(e) => setFaqData({ ...faqData, subtitle: 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"
|
|
placeholder="Find answers to common questions"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
|
<textarea
|
|
value={faqData.content || ''}
|
|
onChange={(e) => setFaqData({ ...faqData, content: e.target.value })}
|
|
rows={20}
|
|
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 font-mono text-sm"
|
|
placeholder="Enter HTML content for FAQ page..."
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
|
<input
|
|
type="text"
|
|
value={faqData.meta_title || ''}
|
|
onChange={(e) => setFaqData({ ...faqData, meta_title: 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"
|
|
placeholder="FAQ - Luxury Hotel"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Description</label>
|
|
<textarea
|
|
value={faqData.meta_description || ''}
|
|
onChange={(e) => setFaqData({ ...faqData, meta_description: 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="Find answers to common questions..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 pt-6 border-t border-gray-200">
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={() => handleSave('faq', faqData)}
|
|
disabled={saving}
|
|
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Save className="w-5 h-5" />
|
|
{saving ? 'Saving...' : 'Save FAQ Content'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* SEO Tab */}
|
|
{activeTab === 'seo' && (
|
|
<div className="space-y-8">
|
|
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
|
<h2 className="text-3xl font-extrabold text-gray-900 mb-6">SEO Optimization</h2>
|
|
|
|
<div className="space-y-6">
|
|
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4 mb-6">
|
|
<p className="text-sm text-blue-800">
|
|
<strong>Note:</strong> SEO settings here apply globally. Individual pages can also have their own SEO settings.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
|
<input
|
|
type="text"
|
|
value={seoData.meta_title || ''}
|
|
onChange={(e) => setSeoData({ ...seoData, meta_title: 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"
|
|
placeholder="Default meta title for the website"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Recommended: 50-60 characters</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Description</label>
|
|
<textarea
|
|
value={seoData.meta_description || ''}
|
|
onChange={(e) => setSeoData({ ...seoData, meta_description: 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="Default meta description for the website"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Recommended: 150-160 characters</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Keywords</label>
|
|
<input
|
|
type="text"
|
|
value={seoData.meta_keywords || ''}
|
|
onChange={(e) => setSeoData({ ...seoData, meta_keywords: 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"
|
|
placeholder="keyword1, keyword2, keyword3"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Comma-separated keywords</p>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 pt-6">
|
|
<h3 className="text-xl font-bold text-gray-900 mb-4">Open Graph (OG) Tags</h3>
|
|
<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">OG Title</label>
|
|
<input
|
|
type="text"
|
|
value={seoData.og_title || ''}
|
|
onChange={(e) => setSeoData({ ...seoData, og_title: 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">OG Image URL</label>
|
|
<input
|
|
type="url"
|
|
value={seoData.og_image || ''}
|
|
onChange={(e) => setSeoData({ ...seoData, og_image: 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"
|
|
placeholder="https://example.com/og-image.jpg"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="mt-6">
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">OG Description</label>
|
|
<textarea
|
|
value={seoData.og_description || ''}
|
|
onChange={(e) => setSeoData({ ...seoData, og_description: 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-700 mb-2">Canonical URL</label>
|
|
<input
|
|
type="url"
|
|
value={seoData.canonical_url || ''}
|
|
onChange={(e) => setSeoData({ ...seoData, canonical_url: 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"
|
|
placeholder="https://example.com"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Default canonical URL for the website</p>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={() => handleSave('seo', seoData)}
|
|
disabled={saving}
|
|
className="px-8 py-3 bg-gradient-to-r from-indigo-500 to-indigo-600 text-white rounded-xl font-semibold hover:from-indigo-600 hover:to-indigo-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Save className="w-5 h-5" />
|
|
{saving ? 'Saving...' : 'Save SEO Settings'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PageContentDashboard;
|
|
|