diff --git a/src/app/dashboard/cms/page.tsx b/src/app/dashboard/cms/page.tsx new file mode 100644 index 0000000..934da11 --- /dev/null +++ b/src/app/dashboard/cms/page.tsx @@ -0,0 +1,596 @@ +'use client'; + +import { useEffect, useState, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import { + cmsService, + getErrorMessage, + CmsContentRecord, + HeroContent, + FeaturesContent, + FeatureItem, + TopProfessionalsContent, + ProfessionalItem, + TestimonialsContent, + TestimonialItem, + StatItem, +} from '@/services'; + +// --------------------------------------------------------------------------- +// Section metadata +// --------------------------------------------------------------------------- + +interface SectionMeta { + 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' }, +]; + +// --------------------------------------------------------------------------- +// Default content factories +// --------------------------------------------------------------------------- + +function defaultHero(): HeroContent { + return { headline: '', description: '', ctaButtonText: '', helperText: '' }; +} + +function defaultFeatures(): FeaturesContent { + return { title: '', subtitle: '', features: [] }; +} + +function defaultTopProfessionals(): TopProfessionalsContent { + return { title: '', ctaText: '', ctaButtonText: '', agents: [], lenders: [] }; +} + +function defaultTestimonials(): TestimonialsContent { + return { title: '', subtitle: '', ratingInfo: '', stats: [], testimonials: [] }; +} + +function defaultContentFor(sectionKey: string): unknown { + switch (sectionKey) { + case 'hero': return defaultHero(); + case 'features': return defaultFeatures(); + case 'topProfessionals': return defaultTopProfessionals(); + case 'testimonials': return defaultTestimonials(); + default: return {}; + } +} + +function emptyFeatureItem(): FeatureItem { + return { iconPath: '', title: '', description: '' }; +} + +function emptyProfessionalItem(): ProfessionalItem { + return { name: '', subtitle: '', location: '', experience: '', expertise: [], imageUrl: '' }; +} + +function emptyTestimonialItem(): TestimonialItem { + return { rating: 5, title: '', content: '', author: '', role: '' }; +} + +function emptyStatItem(): StatItem { + return { iconPath: '', boldText: '', normalText: '' }; +} + +// --------------------------------------------------------------------------- +// Shared input class +// --------------------------------------------------------------------------- +const inputCls = 'w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-[#00293d] bg-white placeholder:text-[#666666]'; +const textareaCls = inputCls; + +// --------------------------------------------------------------------------- +// Page component +// --------------------------------------------------------------------------- + +export default function CmsPage() { + const router = useRouter(); + const [records, setRecords] = 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 [editContent, setEditContent] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [modalError, setModalError] = useState(''); + const [professionalsTab, setProfessionalsTab] = useState<'agents' | 'lenders'>('agents'); + + // Fetch all CMS records for "landing" page + const fetchRecords = useCallback(async () => { + setIsLoading(true); + setError(''); + try { + const data = await cmsService.getByPage('landing'); + setRecords(data); + } catch (err) { + const msg = getErrorMessage(err); + setError(msg); + if (msg.includes('Unauthorized')) { + router.push('/login'); + } + } finally { + setIsLoading(false); + } + }, [router]); + + useEffect(() => { + fetchRecords(); + }, [fetchRecords]); + + // Auto-dismiss notifications after 4 seconds + useEffect(() => { + if (notification) { + const timer = setTimeout(() => setNotification(null), 4000); + return () => clearTimeout(timer); + } + }, [notification]); + + // Helpers + function recordFor(sectionKey: string): CmsContentRecord | undefined { + return records.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)); + setModalError(''); + setProfessionalsTab('agents'); + setEditingSection(sectionKey); + } + + function closeEditor() { + setEditingSection(null); + setEditContent(null); + setModalError(''); + } + + // Save + async function handleSave() { + if (!editingSection || editContent == null) return; + setIsSubmitting(true); + setModalError(''); + try { + await cmsService.upsert({ + pageSlug: 'landing', + sectionKey: editingSection, + content: editContent, + isPublished: true, + }); + closeEditor(); + fetchRecords(); + setNotification({ type: 'success', message: 'Section saved successfully.' }); + } catch (err) { + setModalError(getErrorMessage(err)); + } finally { + setIsSubmitting(false); + } + } + + // --------------------------------------------------------------------------- + // Section-specific form renderers + // --------------------------------------------------------------------------- + + function renderHeroForm() { + const data = editContent as HeroContent; + const update = (patch: Partial) => setEditContent({ ...data, ...patch }); + + return ( +
+
+ +