'use client'; import { useEffect, useState, useCallback, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { cmsService, usersService, uploadService, getErrorMessage, CmsContentRecord, HeroContent, FeaturesContent, FeatureItem, FeaturedAgentItem, TopProfessionalsContent, ProfessionalItem, TestimonialsContent, TestimonialItem, StatItem, AboutHeroContent, AboutStatsContent, AboutStatItem, AboutFeaturesContent, AboutFeatureItem, AboutTeamContent, AboutTeamMember, AboutCtaContent, FaqContent, FaqCategoryItem, FaqItem, User, } from '@/services'; // --------------------------------------------------------------------------- // Section metadata // --------------------------------------------------------------------------- interface SectionMeta { pageSlug: string; sectionKey: string; label: string; description: string; } 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' }, ]; const FAQ_SECTIONS: SectionMeta[] = [ { pageSlug: 'faq', sectionKey: 'faqContent', label: 'FAQ Content', description: 'Categories, questions and answers' }, ]; const CONTACT_SECTIONS: SectionMeta[] = [ { pageSlug: 'contact', sectionKey: 'contactDetails', label: 'Contact Details', description: 'Email, phone, office address and map' }, { pageSlug: 'contact', sectionKey: 'cta', label: 'Call to Action', description: 'Bottom CTA section text and button' }, ]; // --------------------------------------------------------------------------- // Default content factories // --------------------------------------------------------------------------- function defaultHero(): HeroContent { return { headline: '', description: '', ctaButtonText: '', helperText: '' }; } function defaultFeatures(): FeaturesContent { return { title: '', subtitle: '', features: [], featuredAgents: [] }; } function emptyFeaturedAgent(): FeaturedAgentItem { return { name: '', role: '', rating: '', location: '', experience: '', imageUrl: '' }; } function defaultTopProfessionals(): TopProfessionalsContent { return { title: '', ctaText: '', ctaButtonText: '', agents: [], lenders: [] }; } function defaultTestimonials(): TestimonialsContent { return { title: '', subtitle: '', ratingInfo: '', stats: [], testimonials: [] }; } 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; } } if (pageSlug === 'faq') { switch (sectionKey) { case 'faqContent': return { pageTitle: '', categories: [], faqs: [], supportTitle: '', supportDescription: '' } as FaqContent; } } if (pageSlug === 'contact') { switch (sectionKey) { case 'contactDetails': return { title: '', description: '', email: '', phone: '', phoneHours: '', officeAddress: '', officeCity: '', mapUrl: '', directionsUrl: '' }; case 'cta': return { title: '', description: '', buttonText: '', buttonLink: '' }; } } 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 [activePage, setActivePage] = useState<'landing' | 'about' | 'faq' | 'contact'>('landing'); const [records, setRecords] = useState([]); const [aboutRecords, setAboutRecords] = useState([]); const [faqRecords, setFaqRecords] = useState([]); const [contactRecords, setContactRecords] = 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'); const [featuresTab, setFeaturesTab] = useState<'features' | 'agents'>('features'); const [isUploadingImage, setIsUploadingImage] = useState(false); // Agent search const [agentSearchQuery, setAgentSearchQuery] = useState(''); const [agentSearchResults, setAgentSearchResults] = useState([]); const [isSearchingAgents, setIsSearchingAgents] = useState(false); const [showAgentDropdown, setShowAgentDropdown] = useState(false); const agentSearchTimeout = useRef(null); const agentSearchRef = useRef(null); // Fetch all CMS records const fetchRecords = useCallback(async () => { setIsLoading(true); setError(''); try { const [landingData, aboutData, faqData, contactData] = await Promise.all([ cmsService.getByPage('landing'), cmsService.getByPage('about'), cmsService.getByPage('faq'), cmsService.getByPage('contact'), ]); setRecords(landingData); setAboutRecords(aboutData); setFaqRecords(faqData); setContactRecords(contactData); } 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]); // Close agent search dropdown on outside click useEffect(() => { function handleClickOutside(e: MouseEvent) { if (agentSearchRef.current && !agentSearchRef.current.contains(e.target as Node)) { setShowAgentDropdown(false); } } document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // Agent search with debounce — searches only verified agents function handleAgentSearch(query: string) { setAgentSearchQuery(query); if (agentSearchTimeout.current) clearTimeout(agentSearchTimeout.current); if (query.length < 2) { setAgentSearchResults([]); setShowAgentDropdown(false); return; } setIsSearchingAgents(true); setShowAgentDropdown(true); agentSearchTimeout.current = setTimeout(async () => { try { const res = await usersService.getUsers({ role: 'AGENT', search: query, limit: 10, verificationStatus: 'APPROVED' }); // Resolve avatar presigned URLs for display const usersWithAvatars = await Promise.all( res.users.map(async (user) => { if (user.profile?.avatar && !user.profile.avatar.startsWith('http')) { try { const avatarUrl = await uploadService.getPresignedDownloadUrl(user.profile.avatar); return { ...user, profile: { ...user.profile, avatar: avatarUrl } }; } catch { return user; } } return user; }) ); setAgentSearchResults(usersWithAvatars); } catch { setAgentSearchResults([]); } finally { setIsSearchingAgents(false); } }, 300); } function selectAgent(user: User, onChange: (items: ProfessionalItem[]) => void, currentItems: ProfessionalItem[]) { if (currentItems.length >= 3) return; const agent = user.agentProfile; const profile = user.profile; const name = `${profile?.firstName || ''} ${profile?.lastName || ''}`.trim() || user.email; const subtitle = agent?.agentType?.name || agent?.headline || ''; const location = [profile?.city, profile?.state].filter(Boolean).join(', '); const experience = agent?.yearsOfExperience ? `${agent.yearsOfExperience}+ years in real estate.` : ''; const imageUrl = profile?.avatar || ''; const isVerified = agent?.isVerified || false; const newItem: ProfessionalItem = { name, subtitle, location, experience, expertise: [], imageUrl }; onChange([...currentItems, newItem]); setAgentSearchQuery(''); setAgentSearchResults([]); setShowAgentDropdown(false); } // Select agent for Featured Agent Cards (different format) function selectFeaturedAgent(user: User) { const data = editContent as FeaturesContent; if ((data.featuredAgents || []).length >= 3) return; const agent = user.agentProfile; const profile = user.profile; const name = `${profile?.firstName || ''} ${profile?.lastName || ''}`.trim() || user.email; const role = agent?.agentType?.name || agent?.headline || ''; const location = [profile?.city, profile?.state].filter(Boolean).join(', '); const experience = agent?.yearsOfExperience ? `${agent.yearsOfExperience}+ years in real estate.` : ''; const rating = ''; const imageUrl = profile?.avatar || ''; const newAgent: FeaturedAgentItem = { name, role, rating, location, experience, imageUrl }; setEditContent({ ...data, featuredAgents: [...(data.featuredAgents || []), newAgent] }); setAgentSearchQuery(''); setAgentSearchResults([]); setShowAgentDropdown(false); } // Helpers function recordFor(pageSlug: string, sectionKey: string): CmsContentRecord | undefined { const source = pageSlug === 'about' ? aboutRecords : pageSlug === 'faq' ? faqRecords : pageSlug === 'contact' ? contactRecords : records; return source.find((r) => r.sectionKey === sectionKey); } // Open modal 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(section); } function closeEditor() { setEditingSection(null); setEditContent(null); setModalError(''); } // Save async function handleSave() { if (!editingSection || editContent == null) return; setIsSubmitting(true); setModalError(''); try { await cmsService.upsert({ pageSlug: editingSection.pageSlug, sectionKey: editingSection.sectionKey, content: editContent, isPublished: true, }); closeEditor(); fetchRecords(); setNotification({ type: 'success', message: 'Section saved successfully.' }); } catch (err) { setModalError(getErrorMessage(err)); } finally { setIsSubmitting(false); } } // Generic image upload — returns the S3 key for permanent storage 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); return key; } catch (err) { setModalError(getErrorMessage(err)); return ''; } finally { setIsUploadingImage(false); } } // Resolve an S3 key or URL for display — returns a viewable URL async function resolveImageUrl(value: string): Promise { if (!value) return ''; // Already a full URL or local asset path — use directly if (value.startsWith('http') || value.startsWith('/')) return value; // Treat as S3 key — get presigned download URL try { return await uploadService.getPresignedDownloadUrl(value); } catch { return value; } } // --------------------------------------------------------------------------- // Section-specific form renderers // --------------------------------------------------------------------------- function renderHeroForm() { const data = editContent as HeroContent; const update = (patch: Partial) => setEditContent({ ...data, ...patch }); return (