diff --git a/src/app/dashboard/cms/page.tsx b/src/app/dashboard/cms/page.tsx index 29b9aee..a7e7123 100644 --- a/src/app/dashboard/cms/page.tsx +++ b/src/app/dashboard/cms/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'; import { cmsService, usersService, + uploadService, getErrorMessage, CmsContentRecord, HeroContent, @@ -15,6 +16,14 @@ import { TestimonialsContent, TestimonialItem, StatItem, + AboutHeroContent, + AboutStatsContent, + AboutStatItem, + AboutFeaturesContent, + AboutFeatureItem, + AboutTeamContent, + AboutTeamMember, + AboutCtaContent, User, } from '@/services'; @@ -23,16 +32,25 @@ import { // --------------------------------------------------------------------------- interface SectionMeta { + pageSlug: string; sectionKey: string; label: string; description: string; } -const SECTIONS: SectionMeta[] = [ - { sectionKey: 'hero', label: 'Hero', description: 'Main headline, description and call-to-action' }, - { sectionKey: 'features', label: 'Features', description: 'Feature highlights with icons' }, - { sectionKey: 'topProfessionals', label: 'Top Professionals', description: 'Agents and lenders showcase' }, - { sectionKey: 'testimonials', label: 'Testimonials', description: 'Reviews, stats and social proof' }, +const LANDING_SECTIONS: SectionMeta[] = [ + { pageSlug: 'landing', sectionKey: 'hero', label: 'Hero', description: 'Main headline, description and call-to-action' }, + { pageSlug: 'landing', sectionKey: 'features', label: 'Features', description: 'Feature highlights with icons' }, + { pageSlug: 'landing', sectionKey: 'topProfessionals', label: 'Top Professionals', description: 'Agents and lenders showcase' }, + { pageSlug: 'landing', sectionKey: 'testimonials', label: 'Testimonials', description: 'Reviews, stats and social proof' }, +]; + +const ABOUT_SECTIONS: SectionMeta[] = [ + { pageSlug: 'about', sectionKey: 'hero', label: 'Hero & Banner', description: 'Headline, description, CTA and banner image' }, + { pageSlug: 'about', sectionKey: 'stats', label: 'Stats', description: 'Key metrics like verified agents count' }, + { pageSlug: 'about', sectionKey: 'features', label: 'Why Choose Us', description: 'Feature cards with icons' }, + { pageSlug: 'about', sectionKey: 'team', label: 'Team', description: 'Team members with photos' }, + { pageSlug: 'about', sectionKey: 'cta', label: 'Call to Action', description: 'Bottom CTA section' }, ]; // --------------------------------------------------------------------------- @@ -55,7 +73,16 @@ function defaultTestimonials(): TestimonialsContent { return { title: '', subtitle: '', ratingInfo: '', stats: [], testimonials: [] }; } -function defaultContentFor(sectionKey: string): unknown { +function defaultContentFor(pageSlug: string, sectionKey: string): unknown { + if (pageSlug === 'about') { + switch (sectionKey) { + case 'hero': return { badge: '', headline: '', description: '', ctaButtonText: '', bannerImageUrl: '', bannerOverlayText: '' } as AboutHeroContent; + case 'stats': return { stats: [] } as AboutStatsContent; + case 'features': return { badge: '', features: [] } as AboutFeaturesContent; + case 'team': return { title: '', subtitle: '', members: [] } as AboutTeamContent; + case 'cta': return { title: '', description: '', buttonText: '' } as AboutCtaContent; + } + } switch (sectionKey) { case 'hero': return defaultHero(); case 'features': return defaultFeatures(); @@ -93,17 +120,20 @@ const textareaCls = inputCls; export default function CmsPage() { const router = useRouter(); + const [activePage, setActivePage] = useState<'landing' | 'about'>('landing'); const [records, setRecords] = useState([]); + const [aboutRecords, setAboutRecords] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(''); const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null); // Modal - const [editingSection, setEditingSection] = useState(null); + const [editingSection, setEditingSection] = useState(null); const [editContent, setEditContent] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [modalError, setModalError] = useState(''); const [professionalsTab, setProfessionalsTab] = useState<'agents' | 'lenders'>('agents'); + const [isUploadingImage, setIsUploadingImage] = useState(false); // Agent search const [agentSearchQuery, setAgentSearchQuery] = useState(''); @@ -113,13 +143,17 @@ export default function CmsPage() { const agentSearchTimeout = useRef(null); const agentSearchRef = useRef(null); - // Fetch all CMS records for "landing" page + // Fetch all CMS records const fetchRecords = useCallback(async () => { setIsLoading(true); setError(''); try { - const data = await cmsService.getByPage('landing'); - setRecords(data); + const [landingData, aboutData] = await Promise.all([ + cmsService.getByPage('landing'), + cmsService.getByPage('about'), + ]); + setRecords(landingData); + setAboutRecords(aboutData); } catch (err) { const msg = getErrorMessage(err); setError(msg); @@ -195,20 +229,21 @@ export default function CmsPage() { } // Helpers - function recordFor(sectionKey: string): CmsContentRecord | undefined { - return records.find((r) => r.sectionKey === sectionKey); + function recordFor(pageSlug: string, sectionKey: string): CmsContentRecord | undefined { + const source = pageSlug === 'about' ? aboutRecords : records; + return source.find((r) => r.sectionKey === sectionKey); } // Open modal - function openEditor(sectionKey: string) { - const existing = recordFor(sectionKey); - setEditContent(existing ? JSON.parse(JSON.stringify(existing.content)) : defaultContentFor(sectionKey)); + function openEditor(section: SectionMeta) { + const existing = recordFor(section.pageSlug, section.sectionKey); + setEditContent(existing ? JSON.parse(JSON.stringify(existing.content)) : defaultContentFor(section.pageSlug, section.sectionKey)); setModalError(''); setProfessionalsTab('agents'); setAgentSearchQuery(''); setAgentSearchResults([]); setShowAgentDropdown(false); - setEditingSection(sectionKey); + setEditingSection(section); } function closeEditor() { @@ -224,8 +259,8 @@ export default function CmsPage() { setModalError(''); try { await cmsService.upsert({ - pageSlug: 'landing', - sectionKey: editingSection, + pageSlug: editingSection.pageSlug, + sectionKey: editingSection.sectionKey, content: editContent, isPublished: true, }); @@ -239,6 +274,23 @@ export default function CmsPage() { } } + // Generic image upload — returns the presigned download URL + async function handleImageUpload(file: File): Promise { + setIsUploadingImage(true); + setModalError(''); + try { + const { uploadUrl, key } = await uploadService.getPresignedUploadUrl(file.name, file.type, 'properties'); + await uploadService.uploadFileToS3(uploadUrl, file); + const downloadUrl = await uploadService.getPresignedDownloadUrl(key); + return downloadUrl; + } catch (err) { + setModalError(getErrorMessage(err)); + return ''; + } finally { + setIsUploadingImage(false); + } + } + // --------------------------------------------------------------------------- // Section-specific form renderers // --------------------------------------------------------------------------- @@ -582,9 +634,211 @@ export default function CmsPage() { ); } + // Reusable image upload field + function renderImageUploadField(label: string, currentUrl: string, onUrlChange: (url: string) => void) { + return ( +
+ + {currentUrl && ( +
+ Preview { (e.target as HTMLImageElement).style.display = 'none'; }} /> + +
+ )} +
+ + or enter URL below +
+ onUrlChange(e.target.value)} placeholder="https://..." /> +
+ ); + } + + // --- About Us form renderers --- + + function renderAboutHeroForm() { + const data = editContent as AboutHeroContent; + const update = (patch: Partial) => setEditContent({ ...data, ...patch }); + return ( +
+
+ + update({ badge: e.target.value })} placeholder="e.g. Our Mission" /> +
+
+ +