feat: Add full CMS support for the 'Contact' page, including sections, default content factories, data fetching, and editing forms.
This commit is contained in:
@@ -60,6 +60,11 @@ const FAQ_SECTIONS: SectionMeta[] = [
|
|||||||
{ pageSlug: 'faq', sectionKey: 'faqContent', label: 'FAQ Content', description: 'Categories, questions and answers' },
|
{ 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
|
// Default content factories
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -95,6 +100,12 @@ function defaultContentFor(pageSlug: string, sectionKey: string): unknown {
|
|||||||
case 'faqContent': return { pageTitle: '', categories: [], faqs: [], supportTitle: '', supportDescription: '' } as FaqContent;
|
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) {
|
switch (sectionKey) {
|
||||||
case 'hero': return defaultHero();
|
case 'hero': return defaultHero();
|
||||||
case 'features': return defaultFeatures();
|
case 'features': return defaultFeatures();
|
||||||
@@ -132,10 +143,11 @@ const textareaCls = inputCls;
|
|||||||
|
|
||||||
export default function CmsPage() {
|
export default function CmsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [activePage, setActivePage] = useState<'landing' | 'about' | 'faq'>('landing');
|
const [activePage, setActivePage] = useState<'landing' | 'about' | 'faq' | 'contact'>('landing');
|
||||||
const [records, setRecords] = useState<CmsContentRecord[]>([]);
|
const [records, setRecords] = useState<CmsContentRecord[]>([]);
|
||||||
const [aboutRecords, setAboutRecords] = useState<CmsContentRecord[]>([]);
|
const [aboutRecords, setAboutRecords] = useState<CmsContentRecord[]>([]);
|
||||||
const [faqRecords, setFaqRecords] = useState<CmsContentRecord[]>([]);
|
const [faqRecords, setFaqRecords] = useState<CmsContentRecord[]>([]);
|
||||||
|
const [contactRecords, setContactRecords] = useState<CmsContentRecord[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
|
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
|
||||||
@@ -161,14 +173,16 @@ export default function CmsPage() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const [landingData, aboutData, faqData] = await Promise.all([
|
const [landingData, aboutData, faqData, contactData] = await Promise.all([
|
||||||
cmsService.getByPage('landing'),
|
cmsService.getByPage('landing'),
|
||||||
cmsService.getByPage('about'),
|
cmsService.getByPage('about'),
|
||||||
cmsService.getByPage('faq'),
|
cmsService.getByPage('faq'),
|
||||||
|
cmsService.getByPage('contact'),
|
||||||
]);
|
]);
|
||||||
setRecords(landingData);
|
setRecords(landingData);
|
||||||
setAboutRecords(aboutData);
|
setAboutRecords(aboutData);
|
||||||
setFaqRecords(faqData);
|
setFaqRecords(faqData);
|
||||||
|
setContactRecords(contactData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = getErrorMessage(err);
|
const msg = getErrorMessage(err);
|
||||||
setError(msg);
|
setError(msg);
|
||||||
@@ -986,6 +1000,80 @@ export default function CmsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderModalContent() {
|
function renderModalContent() {
|
||||||
if (!editingSection || editContent == null) return null;
|
if (!editingSection || editContent == null) return null;
|
||||||
const key = editingSection.sectionKey;
|
const key = editingSection.sectionKey;
|
||||||
@@ -1004,6 +1092,12 @@ export default function CmsPage() {
|
|||||||
case 'faqContent': return renderFaqForm();
|
case 'faqContent': return renderFaqForm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (page === 'contact') {
|
||||||
|
switch (key) {
|
||||||
|
case 'contactDetails': return renderContactDetailsForm();
|
||||||
|
case 'cta': return renderContactCtaForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'hero': return renderHeroForm();
|
case 'hero': return renderHeroForm();
|
||||||
case 'features': return renderFeaturesForm();
|
case 'features': return renderFeaturesForm();
|
||||||
@@ -1017,8 +1111,8 @@ export default function CmsPage() {
|
|||||||
// Render
|
// Render
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const currentSections = activePage === 'landing' ? LANDING_SECTIONS : activePage === 'about' ? ABOUT_SECTIONS : FAQ_SECTIONS;
|
const currentSections = activePage === 'landing' ? LANDING_SECTIONS : activePage === 'about' ? ABOUT_SECTIONS : activePage === 'contact' ? CONTACT_SECTIONS : FAQ_SECTIONS;
|
||||||
const currentRecords = activePage === 'landing' ? records : activePage === 'about' ? aboutRecords : faqRecords;
|
const currentRecords = activePage === 'landing' ? records : activePage === 'about' ? aboutRecords : activePage === 'contact' ? contactRecords : faqRecords;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -1052,6 +1146,13 @@ export default function CmsPage() {
|
|||||||
>
|
>
|
||||||
FAQ
|
FAQ
|
||||||
</button>
|
</button>
|
||||||
|
<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>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user