2026-02-22 21:52:05 +05:30
'use client' ;
2026-02-24 03:16:56 +05:30
import { useEffect , useState , useCallback , useRef } from 'react' ;
2026-02-22 21:52:05 +05:30
import { useRouter } from 'next/navigation' ;
import {
2026-03-30 14:49:00 +05:30
api ,
2026-02-22 21:52:05 +05:30
cmsService ,
2026-02-24 03:16:56 +05:30
usersService ,
2026-02-24 03:30:35 +05:30
uploadService ,
2026-02-22 21:52:05 +05:30
getErrorMessage ,
CmsContentRecord ,
HeroContent ,
FeaturesContent ,
FeatureItem ,
2026-03-23 11:25:23 +05:30
FeaturedAgentItem ,
2026-02-22 21:52:05 +05:30
TopProfessionalsContent ,
ProfessionalItem ,
TestimonialsContent ,
TestimonialItem ,
StatItem ,
2026-02-24 03:30:35 +05:30
AboutHeroContent ,
AboutStatsContent ,
AboutStatItem ,
AboutFeaturesContent ,
AboutFeatureItem ,
AboutTeamContent ,
AboutTeamMember ,
AboutCtaContent ,
2026-02-24 03:53:56 +05:30
FaqContent ,
FaqCategoryItem ,
FaqItem ,
2026-02-24 03:16:56 +05:30
User ,
2026-02-22 21:52:05 +05:30
} from '@/services' ;
// ---------------------------------------------------------------------------
// Section metadata
// ---------------------------------------------------------------------------
interface SectionMeta {
2026-02-24 03:30:35 +05:30
pageSlug : string ;
2026-02-22 21:52:05 +05:30
sectionKey : string ;
label : string ;
description : string ;
}
2026-02-24 03:30:35 +05:30
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' } ,
2026-02-22 21:52:05 +05:30
] ;
2026-02-24 03:53:56 +05:30
const FAQ_SECTIONS : SectionMeta [ ] = [
{ pageSlug : 'faq' , sectionKey : 'faqContent' , label : 'FAQ Content' , description : 'Categories, questions and answers' } ,
] ;
2026-03-19 13:32:44 +05:30
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' } ,
] ;
2026-02-22 21:52:05 +05:30
// ---------------------------------------------------------------------------
// Default content factories
// ---------------------------------------------------------------------------
function defaultHero ( ) : HeroContent {
return { headline : '' , description : '' , ctaButtonText : '' , helperText : '' } ;
}
function defaultFeatures ( ) : FeaturesContent {
2026-03-23 11:25:23 +05:30
return { title : '' , subtitle : '' , features : [ ] , featuredAgents : [ ] } ;
}
function emptyFeaturedAgent ( ) : FeaturedAgentItem {
return { name : '' , role : '' , rating : '' , location : '' , experience : '' , imageUrl : '' } ;
2026-02-22 21:52:05 +05:30
}
function defaultTopProfessionals ( ) : TopProfessionalsContent {
return { title : '' , ctaText : '' , ctaButtonText : '' , agents : [ ] , lenders : [ ] } ;
}
function defaultTestimonials ( ) : TestimonialsContent {
return { title : '' , subtitle : '' , ratingInfo : '' , stats : [ ] , testimonials : [ ] } ;
}
2026-02-24 03:30:35 +05:30
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 ;
}
}
2026-02-24 03:53:56 +05:30
if ( pageSlug === 'faq' ) {
switch ( sectionKey ) {
case 'faqContent' : return { pageTitle : '' , categories : [ ] , faqs : [ ] , supportTitle : '' , supportDescription : '' } as FaqContent ;
}
}
2026-03-19 13:32:44 +05:30
if ( pageSlug === 'contact' ) {
switch ( sectionKey ) {
case 'contactDetails' : return { title : '' , description : '' , email : '' , phone : '' , phoneHours : '' , officeAddress : '' , officeCity : '' , mapUrl : '' , directionsUrl : '' } ;
case 'cta' : return { title : '' , description : '' , buttonText : '' , buttonLink : '' } ;
}
}
2026-02-22 21:52:05 +05:30
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 ( ) ;
2026-03-19 13:32:44 +05:30
const [ activePage , setActivePage ] = useState < 'landing' | 'about' | 'faq' | 'contact' > ( 'landing' ) ;
2026-02-22 21:52:05 +05:30
const [ records , setRecords ] = useState < CmsContentRecord [ ] > ( [ ] ) ;
2026-02-24 03:30:35 +05:30
const [ aboutRecords , setAboutRecords ] = useState < CmsContentRecord [ ] > ( [ ] ) ;
2026-02-24 03:53:56 +05:30
const [ faqRecords , setFaqRecords ] = useState < CmsContentRecord [ ] > ( [ ] ) ;
2026-03-19 13:32:44 +05:30
const [ contactRecords , setContactRecords ] = useState < CmsContentRecord [ ] > ( [ ] ) ;
2026-02-22 21:52:05 +05:30
const [ isLoading , setIsLoading ] = useState ( true ) ;
const [ error , setError ] = useState ( '' ) ;
const [ notification , setNotification ] = useState < { type : 'success' | 'error' ; message : string } | null > ( null ) ;
// Modal
2026-02-24 03:30:35 +05:30
const [ editingSection , setEditingSection ] = useState < SectionMeta | null > ( null ) ;
2026-02-22 21:52:05 +05:30
const [ editContent , setEditContent ] = useState < unknown > ( null ) ;
const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
const [ modalError , setModalError ] = useState ( '' ) ;
const [ professionalsTab , setProfessionalsTab ] = useState < 'agents' | 'lenders' > ( 'agents' ) ;
2026-03-23 11:25:23 +05:30
const [ featuresTab , setFeaturesTab ] = useState < 'features' | 'agents' > ( 'features' ) ;
2026-02-24 03:30:35 +05:30
const [ isUploadingImage , setIsUploadingImage ] = useState ( false ) ;
2026-02-22 21:52:05 +05:30
2026-02-24 03:16:56 +05:30
// Agent search
const [ agentSearchQuery , setAgentSearchQuery ] = useState ( '' ) ;
const [ agentSearchResults , setAgentSearchResults ] = useState < User [ ] > ( [ ] ) ;
const [ isSearchingAgents , setIsSearchingAgents ] = useState ( false ) ;
const [ showAgentDropdown , setShowAgentDropdown ] = useState ( false ) ;
const agentSearchTimeout = useRef < NodeJS.Timeout | null > ( null ) ;
const agentSearchRef = useRef < HTMLDivElement > ( null ) ;
2026-02-24 03:30:35 +05:30
// Fetch all CMS records
2026-02-22 21:52:05 +05:30
const fetchRecords = useCallback ( async ( ) = > {
setIsLoading ( true ) ;
setError ( '' ) ;
try {
2026-03-19 13:32:44 +05:30
const [ landingData , aboutData , faqData , contactData ] = await Promise . all ( [
2026-02-24 03:30:35 +05:30
cmsService . getByPage ( 'landing' ) ,
cmsService . getByPage ( 'about' ) ,
2026-02-24 03:53:56 +05:30
cmsService . getByPage ( 'faq' ) ,
2026-03-19 13:32:44 +05:30
cmsService . getByPage ( 'contact' ) ,
2026-02-24 03:30:35 +05:30
] ) ;
setRecords ( landingData ) ;
setAboutRecords ( aboutData ) ;
2026-02-24 03:53:56 +05:30
setFaqRecords ( faqData ) ;
2026-03-19 13:32:44 +05:30
setContactRecords ( contactData ) ;
2026-02-22 21:52:05 +05:30
} 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 ] ) ;
2026-02-24 03:16:56 +05:30
// 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 ) ;
} , [ ] ) ;
2026-03-27 11:59:17 +05:30
// Agent search with debounce — searches only verified agents
2026-02-24 03:16:56 +05:30
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 {
2026-03-27 11:59:17 +05:30
const res = await usersService . getUsers ( { role : 'AGENT' , search : query , limit : 10 , verificationStatus : 'APPROVED' } ) ;
2026-03-30 11:58:59 +05:30
// Resolve avatar presigned URLs for dropdown display only
// Store resolved URLs separately so original S3 keys are preserved for CMS storage
const usersWithDisplayAvatars = await Promise . all (
2026-03-29 03:51:55 +05:30
res . users . map ( async ( user ) = > {
if ( user . profile ? . avatar && ! user . profile . avatar . startsWith ( 'http' ) ) {
try {
2026-03-30 11:58:59 +05:30
const displayUrl = await uploadService . getPresignedDownloadUrl ( user . profile . avatar ) ;
return { . . . user , _displayAvatar : displayUrl } ;
2026-03-29 03:51:55 +05:30
} catch {
2026-03-30 11:58:59 +05:30
return { . . . user , _displayAvatar : undefined } ;
2026-03-29 03:51:55 +05:30
}
}
2026-03-30 11:58:59 +05:30
return { . . . user , _displayAvatar : user.profile?.avatar || undefined } ;
2026-03-29 03:51:55 +05:30
} )
) ;
2026-03-30 11:58:59 +05:30
setAgentSearchResults ( usersWithDisplayAvatars as any ) ;
2026-02-24 03:16:56 +05:30
} catch {
setAgentSearchResults ( [ ] ) ;
} finally {
setIsSearchingAgents ( false ) ;
}
} , 300 ) ;
}
2026-03-30 14:49:00 +05:30
async function selectAgent ( user : User , onChange : ( items : ProfessionalItem [ ] ) = > void , currentItems : ProfessionalItem [ ] ) {
2026-02-24 03:16:56 +05:30
if ( currentItems . length >= 3 ) return ;
const agent = user . agentProfile ;
2026-03-29 03:51:55 +05:30
const profile = user . profile ;
const name = ` ${ profile ? . firstName || '' } ${ profile ? . lastName || '' } ` . trim ( ) || user . email ;
const subtitle = agent ? . agentType ? . name || agent ? . headline || '' ;
2026-02-24 03:16:56 +05:30
const imageUrl = profile ? . avatar || '' ;
2026-04-08 04:12:40 +05:30
// Helper to format snake_case to Title Case
const formatLabel = ( v : string ) = >
v . split ( '_' ) . map ( w = > w . charAt ( 0 ) . toUpperCase ( ) + w . slice ( 1 ) . toLowerCase ( ) ) . join ( ' ' ) ;
// Helper to stringify a field value
const valueToString = ( val : any ) : string = > {
if ( val == null ) return '' ;
if ( Array . isArray ( val ) ) {
return val . map ( ( v ) = > ( typeof v === 'object' ? ( v . label || v . name || JSON . stringify ( v ) ) : formatLabel ( String ( v ) ) ) ) . join ( ', ' ) ;
}
if ( typeof val === 'object' ) return val . label || val . name || JSON . stringify ( val ) ;
return formatLabel ( String ( val ) ) ;
} ;
2026-03-30 14:49:00 +05:30
// Fetch full agent profile with field values for location/experience/expertise
let location = [ profile ? . city , profile ? . state ] . filter ( Boolean ) . join ( ', ' ) ;
let experience = agent ? . yearsOfExperience ? ` ${ agent . yearsOfExperience } + years in real estate. ` : '' ;
let expertise : string [ ] = [ ] ;
2026-04-08 04:12:40 +05:30
// Specific slugs to look for (matches profile field seed)
const EXPERIENCE_SLUGS = [ 'years_in_business' , 'years_of_experience' ] ;
const EXPERTISE_SLUGS = [ 'about_me_expertise' , 'expertise_areas' ] ;
2026-03-30 14:49:00 +05:30
if ( agent ? . id ) {
try {
const res = await api . get ( ` /agents/ ${ agent . id } /field-values ` ) ;
const fieldValues = res . data ? . data ? . fieldValues || res . data ? . data || [ ] ;
for ( const fv of fieldValues ) {
2026-04-08 04:12:40 +05:30
const slug : string = ( fv . fieldSlug || fv . field ? . slug || '' ) . toLowerCase ( ) ;
const value = fv . value ? ? fv . jsonValue ? ? fv . textValue ;
if ( value == null || value === '' ) continue ;
// Location: city/state fields
if ( ( slug === 'city' || slug === 'state' ) && ! location ) {
location = Array . isArray ( value ) ? value . join ( ', ' ) : String ( value ) ;
2026-03-30 14:49:00 +05:30
}
2026-04-08 04:12:40 +05:30
// Experience: only specific known slugs (backend already resolves option labels)
if ( ! experience && EXPERIENCE_SLUGS . includes ( slug ) ) {
experience = Array . isArray ( value ) ? value . join ( ', ' ) : String ( value ) ;
2026-03-30 14:49:00 +05:30
}
2026-04-08 04:12:40 +05:30
// Expertise: only specific known slugs (backend already resolves option labels)
if ( EXPERTISE_SLUGS . includes ( slug ) ) {
if ( Array . isArray ( value ) ) {
expertise . push ( . . . value . map ( ( v : any ) = > typeof v === 'object' ? ( v . label || v . name || String ( v ) ) : String ( v ) ) ) ;
} else if ( typeof value === 'string' && value . trim ( ) ) {
expertise . push ( value ) ;
}
2026-03-30 14:49:00 +05:30
}
}
} catch {
// Use basic profile data as fallback
}
}
const newItem : ProfessionalItem = { name , subtitle , location , experience , expertise , imageUrl } ;
2026-02-24 03:16:56 +05:30
onChange ( [ . . . currentItems , newItem ] ) ;
setAgentSearchQuery ( '' ) ;
setAgentSearchResults ( [ ] ) ;
setShowAgentDropdown ( false ) ;
}
2026-03-27 11:59:17 +05:30
// Select agent for Featured Agent Cards (different format)
2026-03-30 14:49:00 +05:30
async function selectFeaturedAgent ( user : User ) {
2026-03-27 11:59:17 +05:30
const data = editContent as FeaturesContent ;
if ( ( data . featuredAgents || [ ] ) . length >= 3 ) return ;
const agent = user . agentProfile ;
2026-03-29 03:51:55 +05:30
const profile = user . profile ;
const name = ` ${ profile ? . firstName || '' } ${ profile ? . lastName || '' } ` . trim ( ) || user . email ;
2026-03-27 11:59:17 +05:30
const role = agent ? . agentType ? . name || agent ? . headline || '' ;
const imageUrl = profile ? . avatar || '' ;
2026-04-08 04:12:40 +05:30
// Helper to format snake_case to Title Case
const formatLabel = ( v : string ) = >
v . split ( '_' ) . map ( w = > w . charAt ( 0 ) . toUpperCase ( ) + w . slice ( 1 ) . toLowerCase ( ) ) . join ( ' ' ) ;
const valueToString = ( val : any ) : string = > {
if ( val == null ) return '' ;
if ( Array . isArray ( val ) ) {
return val . map ( ( v ) = > ( typeof v === 'object' ? ( v . label || v . name || JSON . stringify ( v ) ) : formatLabel ( String ( v ) ) ) ) . join ( ', ' ) ;
}
if ( typeof val === 'object' ) return val . label || val . name || JSON . stringify ( val ) ;
return formatLabel ( String ( val ) ) ;
} ;
2026-03-30 14:49:00 +05:30
let location = [ profile ? . city , profile ? . state ] . filter ( Boolean ) . join ( ', ' ) ;
2026-04-08 04:12:40 +05:30
let experience = agent ? . yearsOfExperience ? ` ${ agent . yearsOfExperience } + years ` : '' ;
const rating = agent ? . averageRating ? String ( agent . averageRating ) : '' ;
const EXPERIENCE_SLUGS = [ 'years_in_business' , 'years_of_experience' ] ;
2026-03-30 14:49:00 +05:30
if ( agent ? . id ) {
try {
const res = await api . get ( ` /agents/ ${ agent . id } /field-values ` ) ;
const fieldValues = res . data ? . data ? . fieldValues || res . data ? . data || [ ] ;
for ( const fv of fieldValues ) {
2026-04-08 04:12:40 +05:30
const slug : string = ( fv . fieldSlug || fv . field ? . slug || '' ) . toLowerCase ( ) ;
const value = fv . value ? ? fv . jsonValue ? ? fv . textValue ;
if ( value == null || value === '' ) continue ;
if ( ( slug === 'city' || slug === 'state' ) && ! location ) {
location = Array . isArray ( value ) ? value . join ( ', ' ) : String ( value ) ;
2026-03-30 14:49:00 +05:30
}
2026-04-08 04:12:40 +05:30
if ( ! experience && EXPERIENCE_SLUGS . includes ( slug ) ) {
experience = Array . isArray ( value ) ? value . join ( ', ' ) : String ( value ) ;
2026-03-30 14:49:00 +05:30
}
}
} catch {
// Use basic profile data as fallback
}
}
2026-03-27 11:59:17 +05:30
const newAgent : FeaturedAgentItem = { name , role , rating , location , experience , imageUrl } ;
setEditContent ( { . . . data , featuredAgents : [ . . . ( data . featuredAgents || [ ] ) , newAgent ] } ) ;
setAgentSearchQuery ( '' ) ;
setAgentSearchResults ( [ ] ) ;
setShowAgentDropdown ( false ) ;
}
2026-02-22 21:52:05 +05:30
// Helpers
2026-02-24 03:30:35 +05:30
function recordFor ( pageSlug : string , sectionKey : string ) : CmsContentRecord | undefined {
2026-03-28 20:13:33 +05:30
const source = pageSlug === 'about' ? aboutRecords : pageSlug === 'faq' ? faqRecords : pageSlug === 'contact' ? contactRecords : records ;
2026-02-24 03:30:35 +05:30
return source . find ( ( r ) = > r . sectionKey === sectionKey ) ;
2026-02-22 21:52:05 +05:30
}
// Open modal
2026-02-24 03:30:35 +05:30
function openEditor ( section : SectionMeta ) {
const existing = recordFor ( section . pageSlug , section . sectionKey ) ;
setEditContent ( existing ? JSON . parse ( JSON . stringify ( existing . content ) ) : defaultContentFor ( section . pageSlug , section . sectionKey ) ) ;
2026-02-22 21:52:05 +05:30
setModalError ( '' ) ;
setProfessionalsTab ( 'agents' ) ;
2026-02-24 03:16:56 +05:30
setAgentSearchQuery ( '' ) ;
setAgentSearchResults ( [ ] ) ;
setShowAgentDropdown ( false ) ;
2026-02-24 03:30:35 +05:30
setEditingSection ( section ) ;
2026-02-22 21:52:05 +05:30
}
function closeEditor() {
setEditingSection ( null ) ;
setEditContent ( null ) ;
setModalError ( '' ) ;
}
// Save
2026-04-07 20:14:44 +05:30
function validateContent ( ) : string | null {
if ( ! editContent ) return null ;
const c = editContent as any ;
// Validate Featured Agents (all fields required)
if ( Array . isArray ( c . featuredAgents ) ) {
for ( let i = 0 ; i < c . featuredAgents . length ; i ++ ) {
const a = c . featuredAgents [ i ] ;
if ( ! a . name ? . trim ( ) ) return ` Featured Agent # ${ i + 1 } : Name is required ` ;
if ( ! a . role ? . trim ( ) ) return ` Featured Agent # ${ i + 1 } : Role is required ` ;
if ( ! a . rating ? . toString ( ) . trim ( ) ) return ` Featured Agent # ${ i + 1 } : Rating is required ` ;
if ( ! a . location ? . trim ( ) ) return ` Featured Agent # ${ i + 1 } : Location is required ` ;
if ( ! a . experience ? . trim ( ) ) return ` Featured Agent # ${ i + 1 } : Experience is required ` ;
if ( ! a . imageUrl ? . trim ( ) ) return ` Featured Agent # ${ i + 1 } : Photo is required ` ;
}
}
// Validate Features
if ( Array . isArray ( c . features ) ) {
for ( let i = 0 ; i < c . features . length ; i ++ ) {
const f = c . features [ i ] ;
if ( ! f . title ? . trim ( ) ) return ` Feature # ${ i + 1 } : Title is required ` ;
if ( ! f . description ? . trim ( ) ) return ` Feature # ${ i + 1 } : Description is required ` ;
if ( ! f . iconPath ? . trim ( ) ) return ` Feature # ${ i + 1 } : Icon is required ` ;
}
}
// Validate Top Professionals agents (all fields required)
if ( Array . isArray ( c . agents ) ) {
for ( let i = 0 ; i < c . agents . length ; i ++ ) {
const a = c . agents [ i ] ;
if ( ! a . name ? . trim ( ) ) return ` Agent # ${ i + 1 } : Name is required ` ;
if ( ! a . subtitle ? . trim ( ) ) return ` Agent # ${ i + 1 } : Subtitle is required ` ;
if ( ! a . location ? . trim ( ) ) return ` Agent # ${ i + 1 } : Location is required ` ;
if ( ! a . experience ? . trim ( ) ) return ` Agent # ${ i + 1 } : Experience is required ` ;
if ( ! Array . isArray ( a . expertise ) || a . expertise . length === 0 || a . expertise . every ( ( e : string ) = > ! e ? . trim ( ) ) ) {
return ` Agent # ${ i + 1 } : At least one expertise is required ` ;
}
if ( ! a . imageUrl ? . trim ( ) ) return ` Agent # ${ i + 1 } : Photo is required ` ;
}
}
// Validate Top Professionals lenders (all fields required)
if ( Array . isArray ( c . lenders ) ) {
for ( let i = 0 ; i < c . lenders . length ; i ++ ) {
const l = c . lenders [ i ] ;
if ( ! l . name ? . trim ( ) ) return ` Lender # ${ i + 1 } : Name is required ` ;
if ( ! l . subtitle ? . trim ( ) ) return ` Lender # ${ i + 1 } : Subtitle is required ` ;
if ( ! l . location ? . trim ( ) ) return ` Lender # ${ i + 1 } : Location is required ` ;
if ( ! l . experience ? . trim ( ) ) return ` Lender # ${ i + 1 } : Experience is required ` ;
if ( ! Array . isArray ( l . expertise ) || l . expertise . length === 0 || l . expertise . every ( ( e : string ) = > ! e ? . trim ( ) ) ) {
return ` Lender # ${ i + 1 } : At least one expertise is required ` ;
}
if ( ! l . imageUrl ? . trim ( ) ) return ` Lender # ${ i + 1 } : Photo is required ` ;
}
}
// Validate testimonials
if ( Array . isArray ( c . testimonials ) ) {
for ( let i = 0 ; i < c . testimonials . length ; i ++ ) {
const t = c . testimonials [ i ] ;
if ( ! t . author ? . trim ( ) ) return ` Testimonial # ${ i + 1 } : Author is required ` ;
if ( ! t . content ? . trim ( ) ) return ` Testimonial # ${ i + 1 } : Content is required ` ;
if ( ! t . title ? . trim ( ) ) return ` Testimonial # ${ i + 1 } : Title is required ` ;
if ( ! t . role ? . trim ( ) ) return ` Testimonial # ${ i + 1 } : Role is required ` ;
}
}
return null ;
}
2026-02-22 21:52:05 +05:30
async function handleSave() {
if ( ! editingSection || editContent == null ) return ;
2026-04-07 20:14:44 +05:30
// Validate before saving
const validationError = validateContent ( ) ;
if ( validationError ) {
setModalError ( validationError ) ;
return ;
}
2026-02-22 21:52:05 +05:30
setIsSubmitting ( true ) ;
setModalError ( '' ) ;
try {
await cmsService . upsert ( {
2026-02-24 03:30:35 +05:30
pageSlug : editingSection.pageSlug ,
sectionKey : editingSection.sectionKey ,
2026-02-22 21:52:05 +05:30
content : editContent ,
isPublished : true ,
} ) ;
closeEditor ( ) ;
fetchRecords ( ) ;
setNotification ( { type : 'success' , message : 'Section saved successfully.' } ) ;
} catch ( err ) {
setModalError ( getErrorMessage ( err ) ) ;
} finally {
setIsSubmitting ( false ) ;
}
}
2026-03-05 05:51:45 +05:30
// Generic image upload — returns the S3 key for permanent storage
2026-02-24 03:30:35 +05:30
async function handleImageUpload ( file : File ) : Promise < string > {
setIsUploadingImage ( true ) ;
setModalError ( '' ) ;
try {
const { uploadUrl , key } = await uploadService . getPresignedUploadUrl ( file . name , file . type , 'properties' ) ;
await uploadService . uploadFileToS3 ( uploadUrl , file ) ;
2026-03-05 05:51:45 +05:30
return key ;
2026-02-24 03:30:35 +05:30
} catch ( err ) {
setModalError ( getErrorMessage ( err ) ) ;
return '' ;
} finally {
setIsUploadingImage ( false ) ;
}
}
2026-03-05 05:51:45 +05:30
// Resolve an S3 key or URL for display — returns a viewable URL
async function resolveImageUrl ( value : string ) : Promise < string > {
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 ;
}
}
2026-02-22 21:52:05 +05:30
// ---------------------------------------------------------------------------
// Section-specific form renderers
// ---------------------------------------------------------------------------
function renderHeroForm() {
const data = editContent as HeroContent ;
const update = ( patch : Partial < HeroContent > ) = > setEditContent ( { . . . data , . . . patch } ) ;
return (
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Headline < / label >
< textarea rows = { 2 } className = { textareaCls } value = { data . headline } onChange = { ( e ) = > update ( { headline : e.target.value } ) } placeholder = "Main headline text" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Description < / label >
< textarea rows = { 3 } className = { textareaCls } value = { data . description } onChange = { ( e ) = > update ( { description : e.target.value } ) } placeholder = "Sub-heading description" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > CTA Button Text < / label >
< input type = "text" className = { inputCls } value = { data . ctaButtonText } onChange = { ( e ) = > update ( { ctaButtonText : e.target.value } ) } placeholder = "e.g. Get Started" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Helper Text < / label >
< textarea rows = { 2 } className = { textareaCls } value = { data . helperText } onChange = { ( e ) = > update ( { helperText : e.target.value } ) } placeholder = "Small text below CTA" / >
< / div >
< / div >
) ;
}
function renderFeaturesForm() {
const data = editContent as FeaturesContent ;
const update = ( patch : Partial < FeaturesContent > ) = > setEditContent ( { . . . data , . . . patch } ) ;
function updateFeature ( index : number , patch : Partial < FeatureItem > ) {
const features = [ . . . data . features ] ;
features [ index ] = { . . . features [ index ] , . . . patch } ;
update ( { features } ) ;
}
function removeFeature ( index : number ) {
update ( { features : data.features.filter ( ( _ , i ) = > i !== index ) } ) ;
}
function addFeature() {
update ( { features : [ . . . data . features , emptyFeatureItem ( ) ] } ) ;
}
return (
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Title < / label >
< input type = "text" className = { inputCls } value = { data . title } onChange = { ( e ) = > update ( { title : e.target.value } ) } placeholder = "Section title" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Subtitle < / label >
< input type = "text" className = { inputCls } value = { data . subtitle } onChange = { ( e ) = > update ( { subtitle : e.target.value } ) } placeholder = "Section subtitle" / >
< / div >
2026-03-23 11:25:23 +05:30
{ /* Tabs */ }
< div className = "border-b border-[#e5e7eb]" >
< nav className = "flex -mb-px" >
< button
type = "button"
onClick = { ( ) = > setFeaturesTab ( 'features' ) }
className = { ` px-4 py-2 text-sm font-medium border-b-2 transition-colors ${ featuresTab === 'features' ? 'border-[#f5a623] text-[#f5a623]' : 'border-transparent text-[#666666] hover:text-[#00293d]' } ` }
>
Features ( { data . features . length } )
< / button >
< button
type = "button"
onClick = { ( ) = > setFeaturesTab ( 'agents' ) }
className = { ` px-4 py-2 text-sm font-medium border-b-2 transition-colors ${ featuresTab === 'agents' ? 'border-[#f5a623] text-[#f5a623]' : 'border-transparent text-[#666666] hover:text-[#00293d]' } ` }
>
Featured Agents ( { ( data . featuredAgents || [ ] ) . length } )
< / button >
< / nav >
< / div >
{ /* Features Tab */ }
{ featuresTab === 'features' && (
< div >
< div className = "flex items-center justify-between mb-2" >
< label className = "block text-sm font-medium text-[#00293d]" > Features ( { data . features . length } ) < / label >
< button type = "button" onClick = { addFeature } className = "px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors" > + Add Feature < / button >
< / div >
{ data . features . length === 0 && < p className = "text-sm text-[#666666] italic" > No features added yet . < / p > }
< div className = "space-y-3" >
{ data . features . map ( ( feat , idx ) = > (
< div key = { idx } className = "p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-semibold text-[#666666]" > Feature { idx + 1 } < / span >
< button type = "button" onClick = { ( ) = > removeFeature ( idx ) } className = "text-red-500 hover:text-red-700 text-xs font-medium" > Remove < / button >
< / div >
{ renderImageUploadField ( 'Icon' , feat . iconPath , ( url ) = > updateFeature ( idx , { iconPath : url } ) ) }
< input type = "text" className = { inputCls } value = { feat . title } onChange = { ( e ) = > updateFeature ( idx , { title : e.target.value } ) } placeholder = "Feature title" / >
< textarea rows = { 2 } className = { textareaCls } value = { feat . description } onChange = { ( e ) = > updateFeature ( idx , { description : e.target.value } ) } placeholder = "Feature description" / >
< / div >
) ) }
< / div >
2026-02-22 21:52:05 +05:30
< / div >
2026-03-23 11:25:23 +05:30
) }
{ /* Featured Agents Tab */ }
{ featuresTab === 'agents' && (
< div >
2026-03-27 11:59:17 +05:30
{ /* Search agents from database */ }
{ ( data . featuredAgents || [ ] ) . length < 3 && (
< div ref = { agentSearchRef } className = "relative mb-4" >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Search verified agents from database < / label >
< div className = "relative" >
< input
type = "text"
className = { inputCls }
value = { agentSearchQuery }
onChange = { ( e ) = > handleAgentSearch ( e . target . value ) }
onFocus = { ( ) = > { if ( agentSearchResults . length > 0 ) setShowAgentDropdown ( true ) ; } }
placeholder = "Search agents by name or email..."
/ >
{ isSearchingAgents && (
< div className = "absolute right-3 top-1/2 -translate-y-1/2" >
< div className = "animate-spin rounded-full h-4 w-4 border-b-2 border-[#f5a623]" > < / div >
< / div >
) }
< / div >
{ showAgentDropdown && agentSearchQuery . length >= 2 && (
< div className = "absolute z-10 w-full mt-1 bg-white border border-[#e5e7eb] rounded-lg shadow-lg max-h-60 overflow-y-auto" >
{ isSearchingAgents && agentSearchResults . length === 0 ? (
< div className = "px-4 py-3 text-sm text-[#666666]" > Searching . . . < / div >
) : agentSearchResults . length === 0 ? (
< div className = "px-4 py-3 text-sm text-[#666666]" > No verified agents found < / div >
) : (
agentSearchResults . map ( ( user ) = > (
< button
key = { user . id }
type = "button"
onClick = { ( ) = > selectFeaturedAgent ( user ) }
className = "w-full px-4 py-3 text-left hover:bg-[#f9fafb] border-b border-[#e5e7eb] last:border-b-0"
>
< p className = "text-sm font-medium text-[#00293d]" >
{ user . profile ? ` ${ user . profile . firstName } ${ user . profile . lastName } ` . trim ( ) : user . email }
< / p >
< p className = "text-xs text-[#666666]" > { user . email } { user . agentProfile ? . agentType ? . name ? ` • ${ user . agentProfile . agentType . name } ` : '' } < / p >
< / button >
) )
) }
< / div >
) }
< / div >
) }
2026-03-23 11:25:23 +05:30
< div className = "flex items-center justify-between mb-2" >
< label className = "block text-sm font-medium text-[#00293d]" > Featured Agent Cards ( { ( data . featuredAgents || [ ] ) . length } / 3 ) < / label >
{ ( data . featuredAgents || [ ] ) . length < 3 && (
< button type = "button" onClick = { ( ) = > update ( { featuredAgents : [ . . . ( data . featuredAgents || [ ] ) , emptyFeaturedAgent ( ) ] } ) } className = "px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors" > + Add Agent < / button >
) }
< / div >
{ ( data . featuredAgents || [ ] ) . length >= 3 && < p className = "text-xs text-[#666666] mb-2" > Maximum of 3 agents reached . < / p > }
< div className = "space-y-3" >
{ ( data . featuredAgents || [ ] ) . map ( ( agent , idx ) = > (
< div key = { idx } className = "p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-semibold text-[#666666]" > Agent { idx + 1 } < / span >
< button type = "button" onClick = { ( ) = > update ( { featuredAgents : ( data . featuredAgents || [ ] ) . filter ( ( _ , i ) = > i !== idx ) } ) } className = "text-red-500 hover:text-red-700 text-xs font-medium" > Remove < / button >
< / div >
< div className = "grid grid-cols-2 gap-2" >
< input type = "text" className = { inputCls } value = { agent . name } onChange = { ( e ) = > { const agents = [ . . . ( data . featuredAgents || [ ] ) ] ; agents [ idx ] = { . . . agents [ idx ] , name : e.target.value } ; update ( { featuredAgents : agents } ) ; } } placeholder = "Name" / >
< input type = "text" className = { inputCls } value = { agent . role } onChange = { ( e ) = > { const agents = [ . . . ( data . featuredAgents || [ ] ) ] ; agents [ idx ] = { . . . agents [ idx ] , role : e.target.value } ; update ( { featuredAgents : agents } ) ; } } placeholder = "Role (e.g. Lender)" / >
< input type = "text" className = { inputCls } value = { agent . location } onChange = { ( e ) = > { const agents = [ . . . ( data . featuredAgents || [ ] ) ] ; agents [ idx ] = { . . . agents [ idx ] , location : e.target.value } ; update ( { featuredAgents : agents } ) ; } } placeholder = "Location" / >
< input type = "text" className = { inputCls } value = { agent . rating } onChange = { ( e ) = > { const agents = [ . . . ( data . featuredAgents || [ ] ) ] ; agents [ idx ] = { . . . agents [ idx ] , rating : e.target.value } ; update ( { featuredAgents : agents } ) ; } } placeholder = "Rating (e.g. 4.9)" / >
< / div >
< input type = "text" className = { inputCls } value = { agent . experience } onChange = { ( e ) = > { const agents = [ . . . ( data . featuredAgents || [ ] ) ] ; agents [ idx ] = { . . . agents [ idx ] , experience : e.target.value } ; update ( { featuredAgents : agents } ) ; } } placeholder = "Experience (e.g. 7+ years...)" / >
{ renderImageUploadField ( 'Photo' , agent . imageUrl , ( url ) = > { const agents = [ . . . ( data . featuredAgents || [ ] ) ] ; agents [ idx ] = { . . . agents [ idx ] , imageUrl : url } ; update ( { featuredAgents : agents } ) ; } ) }
2026-02-22 21:52:05 +05:30
< / div >
2026-03-23 11:25:23 +05:30
) ) }
< / div >
2026-02-22 21:52:05 +05:30
< / div >
2026-03-23 11:25:23 +05:30
) }
2026-02-22 21:52:05 +05:30
< / div >
) ;
}
function renderProfessionalsList (
items : ProfessionalItem [ ] ,
label : string ,
onChange : ( items : ProfessionalItem [ ] ) = > void ,
) {
function updateItem ( index : number , patch : Partial < ProfessionalItem > ) {
const updated = [ . . . items ] ;
updated [ index ] = { . . . updated [ index ] , . . . patch } ;
onChange ( updated ) ;
}
function removeItem ( index : number ) {
onChange ( items . filter ( ( _ , i ) = > i !== index ) ) ;
}
function addItem() {
2026-02-24 03:16:56 +05:30
if ( items . length >= 3 ) return ;
2026-02-22 21:52:05 +05:30
onChange ( [ . . . items , emptyProfessionalItem ( ) ] ) ;
}
2026-02-24 03:16:56 +05:30
const atMax = items . length >= 3 ;
2026-02-22 21:52:05 +05:30
return (
< div >
2026-02-24 03:16:56 +05:30
{ /* Agent search */ }
{ ! atMax && < div ref = { agentSearchRef } className = "relative mb-4" >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Search agents from database < / label >
< div className = "relative" >
< input
type = "text"
className = { inputCls }
value = { agentSearchQuery }
onChange = { ( e ) = > handleAgentSearch ( e . target . value ) }
onFocus = { ( ) = > { if ( agentSearchResults . length > 0 ) setShowAgentDropdown ( true ) ; } }
placeholder = "Search agents by name or email..."
/ >
{ isSearchingAgents && (
< div className = "absolute right-3 top-1/2 -translate-y-1/2" >
< div className = "animate-spin rounded-full h-4 w-4 border-b-2 border-[#f5a623]" > < / div >
< / div >
) }
< / div >
{ showAgentDropdown && agentSearchQuery . length >= 2 && (
< div className = "absolute z-10 w-full mt-1 bg-white border border-[#e5e7eb] rounded-lg shadow-lg max-h-60 overflow-y-auto" >
{ isSearchingAgents && agentSearchResults . length === 0 ? (
< div className = "px-4 py-3 text-sm text-[#666666]" > Searching . . . < / div >
) : agentSearchResults . length === 0 ? (
< div className = "px-4 py-3 text-sm text-[#666666]" > No agents found < / div >
) : (
agentSearchResults . map ( ( user ) = > (
< button
key = { user . id }
type = "button"
onClick = { ( ) = > selectAgent ( user , onChange , items ) }
className = "w-full text-left px-4 py-2.5 hover:bg-[#f5f9f8] transition-colors border-b border-[#e5e7eb] last:border-b-0"
>
< div className = "flex items-center gap-3" >
< div className = "w-8 h-8 rounded-full bg-[#f5a623]/10 flex items-center justify-center flex-shrink-0 relative overflow-hidden" >
< span className = "text-xs font-semibold text-[#f5a623]" >
{ user . profile ? . firstName ? . [ 0 ] || '' } { user . profile ? . lastName ? . [ 0 ] || '' }
< / span >
2026-03-30 11:58:59 +05:30
{ ( user as any ) . _displayAvatar && (
< img src = { ( user as any ) . _displayAvatar } alt = "" className = "absolute inset-0 w-full h-full rounded-full object-cover" onError = { ( e ) = > { ( e . target as HTMLImageElement ) . style . display = 'none' ; } } / >
2026-02-24 03:16:56 +05:30
) }
< / div >
< div className = "min-w-0" >
< div className = "text-sm font-medium text-[#00293d] truncate" >
{ user . profile ? ` ${ user . profile . firstName } ${ user . profile . lastName } ` : 'No Name' }
< span className = "ml-2 text-xs text-[#666666] font-normal" > { user . email } < / span >
< / div >
< div className = "text-xs text-[#666666] truncate" >
{ [ user . profile ? . city , user . profile ? . state ] . filter ( Boolean ) . join ( ', ' ) || 'No location' }
{ user . agentProfile ? . agentType ? . name && (
< span className = "ml-2 px-1.5 py-0.5 bg-[#f5a623]/10 text-[#f5a623] rounded text-[10px] font-medium" >
{ user . agentProfile . agentType . name }
< / span >
) }
< / div >
< / div >
< / div >
< / button >
) )
) }
< / div >
) }
< / div > }
2026-02-22 21:52:05 +05:30
< div className = "flex items-center justify-between mb-2" >
2026-02-24 03:16:56 +05:30
< label className = "block text-sm font-medium text-[#00293d]" > { label } ( { items . length } / 3 ) < / label >
< button type = "button" onClick = { addItem } disabled = { atMax } className = "px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed" > + Add { label . slice ( 0 , - 1 ) } < / button >
2026-02-22 21:52:05 +05:30
< / div >
2026-02-24 03:16:56 +05:30
{ atMax && < p className = "text-xs text-[#666666] mb-2" > Maximum of 3 { label . toLowerCase ( ) } reached . < / p > }
2026-02-22 21:52:05 +05:30
{ items . length === 0 && < p className = "text-sm text-[#666666] italic" > No { label . toLowerCase ( ) } added yet . < / p > }
< div className = "space-y-3" >
{ items . map ( ( item , idx ) = > (
< div key = { idx } className = "p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-semibold text-[#666666]" > { label . slice ( 0 , - 1 ) } { idx + 1 } < / span >
< button type = "button" onClick = { ( ) = > removeItem ( idx ) } className = "text-red-500 hover:text-red-700 text-xs font-medium" > Remove < / button >
< / div >
< div className = "grid grid-cols-2 gap-2" >
< input type = "text" className = { inputCls } value = { item . name } onChange = { ( e ) = > updateItem ( idx , { name : e.target.value } ) } placeholder = "Name" / >
< input type = "text" className = { inputCls } value = { item . subtitle } onChange = { ( e ) = > updateItem ( idx , { subtitle : e.target.value } ) } placeholder = "Subtitle" / >
< input type = "text" className = { inputCls } value = { item . location } onChange = { ( e ) = > updateItem ( idx , { location : e.target.value } ) } placeholder = "Location" / >
< input type = "text" className = { inputCls } value = { item . experience } onChange = { ( e ) = > updateItem ( idx , { experience : e.target.value } ) } placeholder = "Experience" / >
< / div >
< input type = "text" className = { inputCls } value = { item . expertise . join ( ', ' ) } onChange = { ( e ) = > updateItem ( idx , { expertise : e.target.value.split ( ',' ) . map ( ( s ) = > s . trim ( ) ) . filter ( Boolean ) } ) } placeholder = "Expertise (comma-separated)" / >
< input type = "text" className = { inputCls } value = { item . imageUrl } onChange = { ( e ) = > updateItem ( idx , { imageUrl : e.target.value } ) } placeholder = "Image URL" / >
< / div >
) ) }
< / div >
< / div >
) ;
}
function renderTopProfessionalsForm() {
const data = editContent as TopProfessionalsContent ;
const update = ( patch : Partial < TopProfessionalsContent > ) = > setEditContent ( { . . . data , . . . patch } ) ;
return (
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Title < / label >
< input type = "text" className = { inputCls } value = { data . title } onChange = { ( e ) = > update ( { title : e.target.value } ) } placeholder = "Section title" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > CTA Text < / label >
< input type = "text" className = { inputCls } value = { data . ctaText } onChange = { ( e ) = > update ( { ctaText : e.target.value } ) } placeholder = "Call-to-action text" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > CTA Button Text < / label >
< input type = "text" className = { inputCls } value = { data . ctaButtonText } onChange = { ( e ) = > update ( { ctaButtonText : e.target.value } ) } placeholder = "Button label" / >
< / div >
{ /* Tabs */ }
< div className = "border-b border-[#e5e7eb]" >
< nav className = "flex -mb-px" >
< button
type = "button"
onClick = { ( ) = > setProfessionalsTab ( 'agents' ) }
className = { ` px-4 py-2 text-sm font-medium border-b-2 transition-colors ${ professionalsTab === 'agents' ? 'border-[#f5a623] text-[#f5a623]' : 'border-transparent text-[#666666] hover:text-[#00293d]' } ` }
>
Agents ( { data . agents . length } )
< / button >
< button
type = "button"
onClick = { ( ) = > setProfessionalsTab ( 'lenders' ) }
className = { ` px-4 py-2 text-sm font-medium border-b-2 transition-colors ${ professionalsTab === 'lenders' ? 'border-[#f5a623] text-[#f5a623]' : 'border-transparent text-[#666666] hover:text-[#00293d]' } ` }
>
Lenders ( { data . lenders . length } )
< / button >
< / nav >
< / div >
{ professionalsTab === 'agents' && renderProfessionalsList ( data . agents , 'Agents' , ( agents ) = > update ( { agents } ) ) }
{ professionalsTab === 'lenders' && renderProfessionalsList ( data . lenders , 'Lenders' , ( lenders ) = > update ( { lenders } ) ) }
< / div >
) ;
}
function renderTestimonialsForm() {
const data = editContent as TestimonialsContent ;
const update = ( patch : Partial < TestimonialsContent > ) = > setEditContent ( { . . . data , . . . patch } ) ;
function updateStat ( index : number , patch : Partial < StatItem > ) {
const stats = [ . . . data . stats ] ;
stats [ index ] = { . . . stats [ index ] , . . . patch } ;
update ( { stats } ) ;
}
function removeStat ( index : number ) {
update ( { stats : data.stats.filter ( ( _ , i ) = > i !== index ) } ) ;
}
function addStat() {
update ( { stats : [ . . . data . stats , emptyStatItem ( ) ] } ) ;
}
function updateTestimonial ( index : number , patch : Partial < TestimonialItem > ) {
const testimonials = [ . . . data . testimonials ] ;
testimonials [ index ] = { . . . testimonials [ index ] , . . . patch } ;
update ( { testimonials } ) ;
}
function removeTestimonial ( index : number ) {
update ( { testimonials : data.testimonials.filter ( ( _ , i ) = > i !== index ) } ) ;
}
function addTestimonial() {
update ( { testimonials : [ . . . data . testimonials , emptyTestimonialItem ( ) ] } ) ;
}
return (
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Title < / label >
< input type = "text" className = { inputCls } value = { data . title } onChange = { ( e ) = > update ( { title : e.target.value } ) } placeholder = "Section title" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Subtitle < / label >
< textarea rows = { 2 } className = { textareaCls } value = { data . subtitle } onChange = { ( e ) = > update ( { subtitle : e.target.value } ) } placeholder = "Section subtitle" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Rating Info < / label >
< input type = "text" className = { inputCls } value = { data . ratingInfo } onChange = { ( e ) = > update ( { ratingInfo : e.target.value } ) } placeholder = "e.g. 4.9/5 based on 200+ reviews" / >
< / div >
{ /* Stats */ }
< div >
< div className = "flex items-center justify-between mb-2" >
< label className = "block text-sm font-medium text-[#00293d]" > Stats ( { data . stats . length } ) < / label >
< button type = "button" onClick = { addStat } className = "px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors" > + Add Stat < / button >
< / div >
{ data . stats . length === 0 && < p className = "text-sm text-[#666666] italic" > No stats added yet . < / p > }
< div className = "space-y-3" >
{ data . stats . map ( ( stat , idx ) = > (
< div key = { idx } className = "p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-semibold text-[#666666]" > Stat { idx + 1 } < / span >
< button type = "button" onClick = { ( ) = > removeStat ( idx ) } className = "text-red-500 hover:text-red-700 text-xs font-medium" > Remove < / button >
< / div >
< input type = "text" className = { inputCls } value = { stat . iconPath } onChange = { ( e ) = > updateStat ( idx , { iconPath : e.target.value } ) } placeholder = "Icon path" / >
< div className = "grid grid-cols-2 gap-2" >
< input type = "text" className = { inputCls } value = { stat . boldText } onChange = { ( e ) = > updateStat ( idx , { boldText : e.target.value } ) } placeholder = "Bold text (e.g. 500+)" / >
< input type = "text" className = { inputCls } value = { stat . normalText } onChange = { ( e ) = > updateStat ( idx , { normalText : e.target.value } ) } placeholder = "Normal text (e.g. Properties)" / >
< / div >
< / div >
) ) }
< / div >
< / div >
{ /* Testimonials */ }
< div >
< div className = "flex items-center justify-between mb-2" >
< label className = "block text-sm font-medium text-[#00293d]" > Testimonials ( { data . testimonials . length } ) < / label >
< button type = "button" onClick = { addTestimonial } className = "px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors" > + Add Testimonial < / button >
< / div >
{ data . testimonials . length === 0 && < p className = "text-sm text-[#666666] italic" > No testimonials added yet . < / p > }
< div className = "space-y-3" >
{ data . testimonials . map ( ( item , idx ) = > (
< div key = { idx } className = "p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-semibold text-[#666666]" > Testimonial { idx + 1 } < / span >
< button type = "button" onClick = { ( ) = > removeTestimonial ( idx ) } className = "text-red-500 hover:text-red-700 text-xs font-medium" > Remove < / button >
< / div >
< div className = "grid grid-cols-2 gap-2" >
< div >
< label className = "block text-xs text-[#666666] mb-0.5" > Rating < / label >
2026-03-05 05:39:17 +05:30
< input type = "number" min = { 1 } max = { 5 } step = { 0.1 } className = { inputCls } value = { item . rating || '' } onChange = { ( e ) = > updateTestimonial ( idx , { rating : e.target.value === '' ? 0 : parseFloat ( e . target . value ) } ) } / >
2026-02-22 21:52:05 +05:30
< / div >
< div >
< label className = "block text-xs text-[#666666] mb-0.5" > Title < / label >
< input type = "text" className = { inputCls } value = { item . title } onChange = { ( e ) = > updateTestimonial ( idx , { title : e.target.value } ) } placeholder = "Review title" / >
< / div >
< / div >
< textarea rows = { 2 } className = { textareaCls } value = { item . content } onChange = { ( e ) = > updateTestimonial ( idx , { content : e.target.value } ) } placeholder = "Testimonial content" / >
< div className = "grid grid-cols-2 gap-2" >
< input type = "text" className = { inputCls } value = { item . author } onChange = { ( e ) = > updateTestimonial ( idx , { author : e.target.value } ) } placeholder = "Author name" / >
< input type = "text" className = { inputCls } value = { item . role } onChange = { ( e ) = > updateTestimonial ( idx , { role : e.target.value } ) } placeholder = "Author role" / >
< / div >
< / div >
) ) }
< / div >
< / div >
< / div >
) ;
}
2026-03-05 05:51:45 +05:30
// Reusable image upload field with S3 key resolution for preview
function ImageUploadField ( { label , currentUrl , onUrlChange } : { label : string ; currentUrl : string ; onUrlChange : ( url : string ) = > void } ) {
const [ previewUrl , setPreviewUrl ] = useState ( '' ) ;
useEffect ( ( ) = > {
if ( ! currentUrl ) { setPreviewUrl ( '' ) ; return ; }
// If it's already a full URL or local path, use directly
if ( currentUrl . startsWith ( 'http' ) || currentUrl . startsWith ( '/' ) ) {
setPreviewUrl ( currentUrl ) ;
} else {
// S3 key — resolve to presigned URL for preview
resolveImageUrl ( currentUrl ) . then ( setPreviewUrl ) ;
}
} , [ currentUrl ] ) ;
2026-02-24 03:30:35 +05:30
return (
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > { label } < / label >
2026-03-05 05:51:45 +05:30
{ previewUrl && (
2026-02-24 03:30:35 +05:30
< div className = "mb-3 relative rounded-lg overflow-hidden border border-[#e5e7eb]" >
2026-03-05 05:51:45 +05:30
< img src = { previewUrl } alt = "Preview" className = "w-full h-48 object-cover" onError = { ( e ) = > { ( e . target as HTMLImageElement ) . style . display = 'none' ; } } / >
2026-02-24 03:30:35 +05:30
< button type = "button" onClick = { ( ) = > onUrlChange ( '' ) } className = "absolute top-2 right-2 px-2 py-1 bg-red-600 text-white text-xs rounded-lg hover:bg-red-700 transition-colors" > Remove < / button >
< / div >
) }
< div className = "flex gap-2 mb-2" >
< label className = { ` flex-shrink-0 px-4 py-2 text-sm font-medium rounded-lg transition-colors cursor-pointer ${ isUploadingImage ? 'bg-gray-300 text-gray-500' : 'bg-[#f5a623] hover:bg-[#e09620] text-white' } ` } >
{ isUploadingImage ? 'Uploading...' : 'Upload Image' }
< input type = "file" accept = "image/*" className = "hidden" disabled = { isUploadingImage } onChange = { async ( e ) = > {
const file = e . target . files ? . [ 0 ] ;
if ( file ) { const url = await handleImageUpload ( file ) ; if ( url ) onUrlChange ( url ) ; }
e . target . value = '' ;
} } / >
< / label >
< span className = "text-xs text-[#666666] self-center" > or enter URL below < / span >
< / div >
2026-03-05 05:51:45 +05:30
< input type = "text" className = { inputCls } value = { currentUrl } onChange = { ( e ) = > onUrlChange ( e . target . value ) } placeholder = "S3 key or https://..." / >
2026-02-24 03:30:35 +05:30
< / div >
) ;
}
2026-03-05 05:51:45 +05:30
function renderImageUploadField ( label : string , currentUrl : string , onUrlChange : ( url : string ) = > void ) {
return < ImageUploadField label = { label } currentUrl = { currentUrl } onUrlChange = { onUrlChange } / > ;
}
2026-02-24 03:30:35 +05:30
// --- About Us form renderers ---
function renderAboutHeroForm() {
const data = editContent as AboutHeroContent ;
const update = ( patch : Partial < AboutHeroContent > ) = > setEditContent ( { . . . data , . . . patch } ) ;
return (
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Badge Text < / label >
< input type = "text" className = { inputCls } value = { data . badge } onChange = { ( e ) = > update ( { badge : e.target.value } ) } placeholder = "e.g. Our Mission" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Headline < / label >
< textarea rows = { 2 } className = { textareaCls } value = { data . headline } onChange = { ( e ) = > update ( { headline : e.target.value } ) } placeholder = "Main headline" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Description < / label >
< textarea rows = { 3 } className = { textareaCls } value = { data . description } onChange = { ( e ) = > update ( { description : e.target.value } ) } placeholder = "Sub-heading description" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > CTA Button Text < / label >
< input type = "text" className = { inputCls } value = { data . ctaButtonText } onChange = { ( e ) = > update ( { ctaButtonText : e.target.value } ) } placeholder = "e.g. Find Your Agent" / >
< / div >
{ renderImageUploadField ( 'Banner Image' , data . bannerImageUrl , ( url ) = > update ( { bannerImageUrl : url } ) ) }
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Banner Overlay Text < / label >
< input type = "text" className = { inputCls } value = { data . bannerOverlayText } onChange = { ( e ) = > update ( { bannerOverlayText : e.target.value } ) } placeholder = "e.g. Building the future of property." / >
< / div >
< / div >
) ;
}
function renderAboutStatsForm() {
const data = editContent as AboutStatsContent ;
const update = ( patch : Partial < AboutStatsContent > ) = > setEditContent ( { . . . data , . . . patch } ) ;
function updateStat ( index : number , patch : Partial < AboutStatItem > ) {
const stats = [ . . . data . stats ] ;
stats [ index ] = { . . . stats [ index ] , . . . patch } ;
update ( { stats } ) ;
}
return (
< div className = "space-y-4" >
< div className = "flex items-center justify-between mb-2" >
< label className = "block text-sm font-medium text-[#00293d]" > Stats ( { data . stats . length } ) < / label >
< button type = "button" onClick = { ( ) = > update ( { stats : [ . . . data . stats , { value : '' , label : '' } ] } ) } className = "px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors" > + Add Stat < / button >
< / div >
{ data . stats . length === 0 && < p className = "text-sm text-[#666666] italic" > No stats added yet . < / p > }
< div className = "space-y-3" >
{ data . stats . map ( ( stat , idx ) = > (
< div key = { idx } className = "p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-semibold text-[#666666]" > Stat { idx + 1 } < / span >
< button type = "button" onClick = { ( ) = > update ( { stats : data.stats.filter ( ( _ , i ) = > i !== idx ) } ) } className = "text-red-500 hover:text-red-700 text-xs font-medium" > Remove < / button >
< / div >
< div className = "grid grid-cols-2 gap-2" >
< input type = "text" className = { inputCls } value = { stat . value } onChange = { ( e ) = > updateStat ( idx , { value : e.target.value } ) } placeholder = "Value (e.g. 15k+)" / >
< input type = "text" className = { inputCls } value = { stat . label } onChange = { ( e ) = > updateStat ( idx , { label : e.target.value } ) } placeholder = "Label (e.g. Verified Agents)" / >
< / div >
< / div >
) ) }
< / div >
< / div >
) ;
}
function renderAboutFeaturesForm() {
const data = editContent as AboutFeaturesContent ;
const update = ( patch : Partial < AboutFeaturesContent > ) = > setEditContent ( { . . . data , . . . patch } ) ;
function updateFeature ( index : number , patch : Partial < AboutFeatureItem > ) {
const features = [ . . . data . features ] ;
features [ index ] = { . . . features [ index ] , . . . patch } ;
update ( { features } ) ;
}
return (
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Section Badge < / label >
< input type = "text" className = { inputCls } value = { data . badge } onChange = { ( e ) = > update ( { badge : e.target.value } ) } placeholder = "e.g. Why Choose Us" / >
< / div >
< div className = "flex items-center justify-between mb-2" >
< label className = "block text-sm font-medium text-[#00293d]" > Features ( { data . features . length } ) < / label >
< button type = "button" onClick = { ( ) = > update ( { features : [ . . . data . features , { iconPath : '' , title : '' , description : '' } ] } ) } className = "px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors" > + Add Feature < / button >
< / div >
{ data . features . length === 0 && < p className = "text-sm text-[#666666] italic" > No features added yet . < / p > }
< div className = "space-y-3" >
{ data . features . map ( ( feat , idx ) = > (
< div key = { idx } className = "p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-semibold text-[#666666]" > Feature { idx + 1 } < / span >
< button type = "button" onClick = { ( ) = > update ( { features : data.features.filter ( ( _ , i ) = > i !== idx ) } ) } className = "text-red-500 hover:text-red-700 text-xs font-medium" > Remove < / button >
< / div >
2026-02-24 03:39:19 +05:30
{ renderImageUploadField ( 'Icon' , feat . iconPath , ( url ) = > updateFeature ( idx , { iconPath : url } ) ) }
2026-02-24 03:30:35 +05:30
< input type = "text" className = { inputCls } value = { feat . title } onChange = { ( e ) = > updateFeature ( idx , { title : e.target.value } ) } placeholder = "Feature title" / >
< textarea rows = { 2 } className = { textareaCls } value = { feat . description } onChange = { ( e ) = > updateFeature ( idx , { description : e.target.value } ) } placeholder = "Feature description" / >
< / div >
) ) }
< / div >
< / div >
) ;
}
function renderAboutTeamForm() {
const data = editContent as AboutTeamContent ;
const update = ( patch : Partial < AboutTeamContent > ) = > setEditContent ( { . . . data , . . . patch } ) ;
function updateMember ( index : number , patch : Partial < AboutTeamMember > ) {
const members = [ . . . data . members ] ;
members [ index ] = { . . . members [ index ] , . . . patch } ;
update ( { members } ) ;
}
return (
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Section Title < / label >
< input type = "text" className = { inputCls } value = { data . title } onChange = { ( e ) = > update ( { title : e.target.value } ) } placeholder = "e.g. Meet the minds behind the platform." / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Section Subtitle < / label >
< textarea rows = { 2 } className = { textareaCls } value = { data . subtitle } onChange = { ( e ) = > update ( { subtitle : e.target.value } ) } placeholder = "Section description" / >
< / div >
< div className = "flex items-center justify-between mb-2" >
< label className = "block text-sm font-medium text-[#00293d]" > Team Members ( { data . members . length } ) < / label >
< button type = "button" onClick = { ( ) = > update ( { members : [ . . . data . members , { name : '' , role : '' , imageUrl : '' } ] } ) } className = "px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors" > + Add Member < / button >
< / div >
{ data . members . length === 0 && < p className = "text-sm text-[#666666] italic" > No team members added yet . < / p > }
< div className = "space-y-3" >
{ data . members . map ( ( member , idx ) = > (
< div key = { idx } className = "p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-semibold text-[#666666]" > Member { idx + 1 } < / span >
< button type = "button" onClick = { ( ) = > update ( { members : data.members.filter ( ( _ , i ) = > i !== idx ) } ) } className = "text-red-500 hover:text-red-700 text-xs font-medium" > Remove < / button >
< / div >
< div className = "grid grid-cols-2 gap-2" >
< input type = "text" className = { inputCls } value = { member . name } onChange = { ( e ) = > updateMember ( idx , { name : e.target.value } ) } placeholder = "Name" / >
< input type = "text" className = { inputCls } value = { member . role } onChange = { ( e ) = > updateMember ( idx , { role : e.target.value } ) } placeholder = "Role" / >
< / div >
{ renderImageUploadField ( ` Photo ` , member . imageUrl , ( url ) = > updateMember ( idx , { imageUrl : url } ) ) }
< / div >
) ) }
< / div >
< / div >
) ;
}
function renderAboutCtaForm() {
const data = editContent as AboutCtaContent ;
const update = ( patch : Partial < AboutCtaContent > ) = > setEditContent ( { . . . data , . . . patch } ) ;
return (
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Title < / label >
< input type = "text" className = { inputCls } value = { data . title } onChange = { ( e ) = > update ( { title : e.target.value } ) } placeholder = "e.g. Ready to find an agent?" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Description < / label >
< textarea rows = { 2 } className = { textareaCls } value = { data . description } onChange = { ( e ) = > update ( { description : e.target.value } ) } placeholder = "CTA description" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Button Text < / label >
< input type = "text" className = { inputCls } value = { data . buttonText } onChange = { ( e ) = > update ( { buttonText : e.target.value } ) } placeholder = "e.g. Start Your Search" / >
< / div >
< / div >
) ;
}
2026-02-24 03:53:56 +05:30
function renderFaqForm() {
const data = editContent as FaqContent ;
const update = ( patch : Partial < FaqContent > ) = > setEditContent ( { . . . data , . . . patch } ) ;
function addCategory() {
const newCat : FaqCategoryItem = { id : '' , label : '' } ;
update ( { categories : [ . . . data . categories , newCat ] } ) ;
}
function updateCategory ( index : number , patch : Partial < FaqCategoryItem > ) {
const categories = [ . . . data . categories ] ;
categories [ index ] = { . . . categories [ index ] , . . . patch } ;
update ( { categories } ) ;
}
function removeCategory ( index : number ) {
update ( { categories : data.categories.filter ( ( _ , i ) = > i !== index ) } ) ;
}
function addFaq() {
const newFaq : FaqItem = { id : Date.now ( ) , icon : '' , question : '' , answer : '' , category : '' } ;
update ( { faqs : [ . . . data . faqs , newFaq ] } ) ;
}
function updateFaq ( index : number , patch : Partial < FaqItem > ) {
const faqs = [ . . . data . faqs ] ;
faqs [ index ] = { . . . faqs [ index ] , . . . patch } ;
update ( { faqs } ) ;
}
function removeFaq ( index : number ) {
update ( { faqs : data.faqs.filter ( ( _ , i ) = > i !== index ) } ) ;
}
return (
< div className = "space-y-6" >
{ /* Page Title */ }
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Page Title < / label >
< input type = "text" className = { inputCls } value = { data . pageTitle } onChange = { ( e ) = > update ( { pageTitle : e.target.value } ) } placeholder = "e.g. Frequently Asked Questions ?" / >
< / div >
{ /* Support Title */ }
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Support Title < / label >
< input type = "text" className = { inputCls } value = { data . supportTitle } onChange = { ( e ) = > update ( { supportTitle : e.target.value } ) } placeholder = "e.g. Still Need Help ?" / >
< / div >
{ /* Support Description */ }
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Support Description < / label >
< textarea rows = { 3 } className = { textareaCls } value = { data . supportDescription } onChange = { ( e ) = > update ( { supportDescription : e.target.value } ) } placeholder = "e.g. Our Support Team Is Available Mon-Fri, 9am-6pm IST" / >
< / div >
{ /* Categories */ }
< div >
< div className = "flex items-center justify-between mb-2" >
< label className = "block text-sm font-medium text-[#00293d]" > Categories ( { data . categories . length } ) < / label >
< button type = "button" onClick = { addCategory } className = "px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors" > + Add Category < / button >
< / div >
{ data . categories . length === 0 && < p className = "text-sm text-[#666666] italic" > No categories added yet . < / p > }
< div className = "space-y-3" >
{ data . categories . map ( ( cat , idx ) = > (
< div key = { idx } className = "p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-semibold text-[#666666]" > Category { idx + 1 } < / span >
< button type = "button" onClick = { ( ) = > removeCategory ( idx ) } className = "text-red-500 hover:text-red-700 text-xs font-medium" > Remove < / button >
< / div >
< div className = "grid grid-cols-2 gap-2" >
< input type = "text" className = { inputCls } value = { cat . id } onChange = { ( e ) = > updateCategory ( idx , { id : e.target.value } ) } placeholder = "ID (e.g. buying)" / >
< input type = "text" className = { inputCls } value = { cat . label } onChange = { ( e ) = > updateCategory ( idx , { label : e.target.value } ) } placeholder = "Label (e.g. Buying a Home)" / >
< / div >
< / div >
) ) }
< / div >
< / div >
{ /* FAQ Items */ }
< div >
< div className = "flex items-center justify-between mb-2" >
< label className = "block text-sm font-medium text-[#00293d]" > FAQ Items ( { data . faqs . length } ) < / label >
< button type = "button" onClick = { addFaq } className = "px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors" > + Add FAQ < / button >
< / div >
{ data . faqs . length === 0 && < p className = "text-sm text-[#666666] italic" > No FAQ items added yet . < / p > }
< div className = "space-y-3" >
{ data . faqs . map ( ( faq , idx ) = > (
< div key = { idx } className = "p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-semibold text-[#666666]" > FAQ { idx + 1 } < / span >
< button type = "button" onClick = { ( ) = > removeFaq ( idx ) } className = "text-red-500 hover:text-red-700 text-xs font-medium" > Remove < / button >
< / div >
{ renderImageUploadField ( 'Icon' , faq . icon , ( url ) = > updateFaq ( idx , { icon : url } ) ) }
< div >
< label className = "block text-xs text-[#666666] mb-0.5" > Question < / label >
< input type = "text" className = { inputCls } value = { faq . question } onChange = { ( e ) = > updateFaq ( idx , { question : e.target.value } ) } placeholder = "Enter the question" / >
< / div >
< div >
< label className = "block text-xs text-[#666666] mb-0.5" > Answer < / label >
< textarea rows = { 3 } className = { textareaCls } value = { faq . answer } onChange = { ( e ) = > updateFaq ( idx , { answer : e.target.value } ) } placeholder = "Enter the answer" / >
< / div >
< div >
< label className = "block text-xs text-[#666666] mb-0.5" > Category < / label >
< select
className = { inputCls }
value = { faq . category }
onChange = { ( e ) = > updateFaq ( idx , { category : e.target.value } ) }
>
< option value = "" > Select a category . . . < / option >
{ data . categories . map ( ( cat ) = > (
< option key = { cat . id } value = { cat . id } > { cat . label } < / option >
) ) }
< / select >
< / div >
< / div >
) ) }
< / div >
< / div >
< / div >
) ;
}
2026-03-19 13:32:44 +05:30
// ---------------------------------------------------------------------------
// Contact page editors
// ---------------------------------------------------------------------------
function renderContactDetailsForm() {
const data = editContent as Record < string , string > ;
const update = ( patch : Record < string , string > ) = > setEditContent ( { . . . data , . . . patch } ) ;
return (
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Page Title < / label >
< input className = { inputCls } value = { data . title || '' } onChange = { e = > update ( { title : e.target.value } ) } placeholder = "Get In Touch" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Description < / label >
< textarea className = { textareaCls } rows = { 2 } value = { data . description || '' } onChange = { e = > update ( { description : e.target.value } ) } placeholder = "Page description" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Support Email < / label >
< input className = { inputCls } value = { data . email || '' } onChange = { e = > update ( { email : e.target.value } ) } placeholder = "support@example.com" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Phone Number < / label >
< input className = { inputCls } value = { data . phone || '' } onChange = { e = > update ( { phone : e.target.value } ) } placeholder = "1234567890" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Phone Hours < / label >
< input className = { inputCls } value = { data . phoneHours || '' } onChange = { e = > update ( { phoneHours : e.target.value } ) } placeholder = "Mon-Fri 9am-6pm" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Office Address ( Line 1 ) < / label >
< input className = { inputCls } value = { data . officeAddress || '' } onChange = { e = > update ( { officeAddress : e.target.value } ) } placeholder = "123 Market Street" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Office Address ( Line 2 ) < / label >
< input className = { inputCls } value = { data . officeCity || '' } onChange = { e = > update ( { officeCity : e.target.value } ) } placeholder = "New York CA 234737" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Google Maps Embed URL ( optional ) < / label >
< input className = { inputCls } value = { data . mapUrl || '' } onChange = { e = > update ( { mapUrl : e.target.value } ) } placeholder = "https://maps.google.com/..." / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Directions URL ( optional ) < / label >
< input className = { inputCls } value = { data . directionsUrl || '' } onChange = { e = > update ( { directionsUrl : e.target.value } ) } placeholder = "https://maps.google.com/..." / >
< / div >
< / div >
) ;
}
function renderContactCtaForm() {
const data = editContent as Record < string , string > ;
const update = ( patch : Record < string , string > ) = > setEditContent ( { . . . data , . . . patch } ) ;
return (
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Title < / label >
< input className = { inputCls } value = { data . title || '' } onChange = { e = > update ( { title : e.target.value } ) } placeholder = "Ready to find an agent?" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Description < / label >
< textarea className = { textareaCls } rows = { 2 } value = { data . description || '' } onChange = { e = > update ( { description : e.target.value } ) } placeholder = "CTA description" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Button Text < / label >
< input className = { inputCls } value = { data . buttonText || '' } onChange = { e = > update ( { buttonText : e.target.value } ) } placeholder = "Start Your Search" / >
< / div >
< div >
< label className = "block text-sm font-medium text-[#00293d] mb-1" > Button Link < / label >
< input className = { inputCls } value = { data . buttonLink || '' } onChange = { e = > update ( { buttonLink : e.target.value } ) } placeholder = "/user/profiles" / >
< / div >
< / div >
) ;
}
2026-02-22 21:52:05 +05:30
function renderModalContent() {
if ( ! editingSection || editContent == null ) return null ;
2026-02-24 03:30:35 +05:30
const key = editingSection . sectionKey ;
const page = editingSection . pageSlug ;
if ( page === 'about' ) {
switch ( key ) {
case 'hero' : return renderAboutHeroForm ( ) ;
case 'stats' : return renderAboutStatsForm ( ) ;
case 'features' : return renderAboutFeaturesForm ( ) ;
case 'team' : return renderAboutTeamForm ( ) ;
case 'cta' : return renderAboutCtaForm ( ) ;
}
}
2026-02-24 03:53:56 +05:30
if ( page === 'faq' ) {
switch ( key ) {
case 'faqContent' : return renderFaqForm ( ) ;
}
}
2026-03-19 13:32:44 +05:30
if ( page === 'contact' ) {
switch ( key ) {
case 'contactDetails' : return renderContactDetailsForm ( ) ;
case 'cta' : return renderContactCtaForm ( ) ;
}
}
2026-02-24 03:30:35 +05:30
switch ( key ) {
2026-02-22 21:52:05 +05:30
case 'hero' : return renderHeroForm ( ) ;
case 'features' : return renderFeaturesForm ( ) ;
case 'topProfessionals' : return renderTopProfessionalsForm ( ) ;
case 'testimonials' : return renderTestimonialsForm ( ) ;
default : return < p className = "text-[#666666]" > Unknown section type . < / p > ;
}
}
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
2026-03-19 13:32:44 +05:30
const currentSections = activePage === 'landing' ? LANDING_SECTIONS : activePage === 'about' ? ABOUT_SECTIONS : activePage === 'contact' ? CONTACT_SECTIONS : FAQ_SECTIONS ;
const currentRecords = activePage === 'landing' ? records : activePage === 'about' ? aboutRecords : activePage === 'contact' ? contactRecords : faqRecords ;
2026-02-22 21:52:05 +05:30
return (
< div >
{ /* Page header */ }
< div className = "mb-6" >
< h1 className = "text-2xl font-bold text-[#00293d] font-fractul" > Content Management < / h1 >
2026-02-24 03:30:35 +05:30
< p className = "text-[#666666] font-serif" > Manage website page content < / p >
< / div >
{ /* Page tabs */ }
< div className = "border-b border-[#e5e7eb] mb-6" >
< nav className = "flex -mb-px" >
< button
type = "button"
onClick = { ( ) = > setActivePage ( 'landing' ) }
className = { ` px-5 py-3 text-sm font-medium border-b-2 transition-colors ${ activePage === 'landing' ? 'border-[#f5a623] text-[#f5a623]' : 'border-transparent text-[#666666] hover:text-[#00293d]' } ` }
>
Landing Page
< / button >
< button
type = "button"
onClick = { ( ) = > setActivePage ( 'about' ) }
className = { ` px-5 py-3 text-sm font-medium border-b-2 transition-colors ${ activePage === 'about' ? 'border-[#f5a623] text-[#f5a623]' : 'border-transparent text-[#666666] hover:text-[#00293d]' } ` }
>
About Us
< / button >
2026-02-24 03:53:56 +05:30
< button
type = "button"
onClick = { ( ) = > setActivePage ( 'faq' ) }
className = { ` px-5 py-3 text-sm font-medium border-b-2 transition-colors ${ activePage === 'faq' ? 'border-[#f5a623] text-[#f5a623]' : 'border-transparent text-[#666666] hover:text-[#00293d]' } ` }
>
FAQ
< / button >
2026-03-19 13:32:44 +05:30
< button
type = "button"
onClick = { ( ) = > setActivePage ( 'contact' ) }
className = { ` px-5 py-3 text-sm font-medium border-b-2 transition-colors ${ activePage === 'contact' ? 'border-[#f5a623] text-[#f5a623]' : 'border-transparent text-[#666666] hover:text-[#00293d]' } ` }
>
Contact
< / button >
2026-02-24 03:30:35 +05:30
< / nav >
2026-02-22 21:52:05 +05:30
< / div >
{ /* Notification toast */ }
{ notification && (
< div className = { ` mb-4 p-3 rounded-lg border text-sm ${ notification . type === 'success' ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700' } ` } >
{ notification . message }
< / div >
) }
{ /* Error */ }
{ error && (
< div className = "mb-4 p-3 bg-red-50 border border-red-200 rounded-lg" >
< p className = "text-red-700 text-sm" > { error } < / p >
< / div >
) }
{ /* Loading */ }
{ isLoading ? (
< div className = "flex items-center justify-center py-16" >
< div className = "animate-spin rounded-full h-8 w-8 border-b-2 border-[#f5a623]" > < / div >
< / div >
) : (
/* Section cards grid */
< div className = "grid grid-cols-1 md:grid-cols-2 gap-6" >
2026-02-24 03:30:35 +05:30
{ currentSections . map ( ( section ) = > {
const record = recordFor ( section . pageSlug , section . sectionKey ) ;
2026-02-22 21:52:05 +05:30
const hasContent = ! ! record ;
const isPublished = record ? . isPublished ? ? false ;
return (
2026-02-24 03:30:35 +05:30
< div key = { ` ${ section . pageSlug } - ${ section . sectionKey } ` } className = "bg-white rounded-xl shadow-sm border border-[#e5e7eb]" >
2026-02-22 21:52:05 +05:30
< div className = "px-6 py-4 border-b border-[#e5e7eb] flex items-center justify-between" >
< div >
< h2 className = "text-lg font-semibold text-[#00293d]" > { section . label } < / h2 >
< p className = "text-sm text-[#666666] font-serif" > { section . description } < / p >
< / div >
< div className = "flex items-center space-x-2" >
{ hasContent ? (
isPublished ? (
< span className = "px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800" > Published < / span >
) : (
< span className = "px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800" > Draft < / span >
)
) : (
< span className = "px-2 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-800" > No Content < / span >
) }
< / div >
< / div >
< div className = "px-6 py-4" >
{ hasContent ? (
< p className = "text-sm text-[#666666]" >
Last updated : { new Date ( record . updatedAt ) . toLocaleDateString ( 'en-US' , { year : 'numeric' , month : 'short' , day : 'numeric' , hour : '2-digit' , minute : '2-digit' } ) }
< / p >
) : (
< p className = "text-sm text-[#666666] italic" > No content configured yet . Click edit to add content . < / p >
) }
< div className = "mt-4 flex justify-end" >
< button
2026-02-24 03:30:35 +05:30
onClick = { ( ) = > openEditor ( section ) }
2026-02-22 21:52:05 +05:30
className = "px-4 py-2 bg-[#f5a623] hover:bg-[#e09620] text-white text-sm font-medium rounded-lg transition-colors inline-flex items-center"
>
< svg className = "w-4 h-4 mr-1.5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" / >
< / svg >
Edit
< / button >
< / div >
< / div >
< / div >
) ;
} ) }
< / div >
) }
{ /* Edit modal */ }
{ editingSection && (
< div className = "fixed inset-0 bg-black/50 flex items-center justify-center z-50" >
< div className = "bg-white rounded-xl shadow-xl w-full max-w-2xl mx-4 max-h-[90vh] flex flex-col" >
{ /* Modal header */ }
< div className = "px-6 py-4 border-b border-[#e5e7eb] flex-shrink-0" >
< h3 className = "text-lg font-semibold text-[#00293d]" >
2026-02-24 03:30:35 +05:30
Edit { editingSection . label }
2026-02-22 21:52:05 +05:30
< / h3 >
2026-02-24 03:30:35 +05:30
< p className = "text-sm text-[#666666]" > { editingSection . description } < / p >
2026-02-22 21:52:05 +05:30
< / div >
{ /* Modal body - scrollable */ }
< div className = "px-6 py-4 overflow-y-auto flex-1" >
{ modalError && (
< div className = "mb-4 p-3 bg-red-50 border border-red-200 rounded-lg" >
< p className = "text-red-700 text-sm" > { modalError } < / p >
< / div >
) }
{ renderModalContent ( ) }
< / div >
{ /* Modal footer */ }
< div className = "px-6 py-4 border-t border-[#e5e7eb] flex justify-end space-x-3 flex-shrink-0" >
< button
type = "button"
onClick = { closeEditor }
className = "px-4 py-2 border border-[#e5e7eb] text-[#00293d] rounded-xl hover:bg-[#f5f9f8] transition-colors"
>
Cancel
< / button >
< button
type = "button"
onClick = { handleSave }
disabled = { isSubmitting }
className = "px-4 py-2 bg-[#f5a623] text-white rounded-xl hover:bg-[#e09620] transition-colors disabled:opacity-50"
>
{ isSubmitting ? 'Saving...' : 'Save Changes' }
< / button >
< / div >
< / div >
< / div >
) }
< / div >
) ;
}