feat: Add About Us page CMS functionality with dedicated sections and image upload support.
This commit is contained in:
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
cmsService,
|
||||
usersService,
|
||||
uploadService,
|
||||
getErrorMessage,
|
||||
CmsContentRecord,
|
||||
HeroContent,
|
||||
@@ -15,6 +16,14 @@ import {
|
||||
TestimonialsContent,
|
||||
TestimonialItem,
|
||||
StatItem,
|
||||
AboutHeroContent,
|
||||
AboutStatsContent,
|
||||
AboutStatItem,
|
||||
AboutFeaturesContent,
|
||||
AboutFeatureItem,
|
||||
AboutTeamContent,
|
||||
AboutTeamMember,
|
||||
AboutCtaContent,
|
||||
User,
|
||||
} from '@/services';
|
||||
|
||||
@@ -23,16 +32,25 @@ import {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface SectionMeta {
|
||||
pageSlug: string;
|
||||
sectionKey: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const SECTIONS: SectionMeta[] = [
|
||||
{ sectionKey: 'hero', label: 'Hero', description: 'Main headline, description and call-to-action' },
|
||||
{ sectionKey: 'features', label: 'Features', description: 'Feature highlights with icons' },
|
||||
{ sectionKey: 'topProfessionals', label: 'Top Professionals', description: 'Agents and lenders showcase' },
|
||||
{ sectionKey: 'testimonials', label: 'Testimonials', description: 'Reviews, stats and social proof' },
|
||||
const LANDING_SECTIONS: SectionMeta[] = [
|
||||
{ pageSlug: 'landing', sectionKey: 'hero', label: 'Hero', description: 'Main headline, description and call-to-action' },
|
||||
{ pageSlug: 'landing', sectionKey: 'features', label: 'Features', description: 'Feature highlights with icons' },
|
||||
{ pageSlug: 'landing', sectionKey: 'topProfessionals', label: 'Top Professionals', description: 'Agents and lenders showcase' },
|
||||
{ pageSlug: 'landing', sectionKey: 'testimonials', label: 'Testimonials', description: 'Reviews, stats and social proof' },
|
||||
];
|
||||
|
||||
const ABOUT_SECTIONS: SectionMeta[] = [
|
||||
{ pageSlug: 'about', sectionKey: 'hero', label: 'Hero & Banner', description: 'Headline, description, CTA and banner image' },
|
||||
{ pageSlug: 'about', sectionKey: 'stats', label: 'Stats', description: 'Key metrics like verified agents count' },
|
||||
{ pageSlug: 'about', sectionKey: 'features', label: 'Why Choose Us', description: 'Feature cards with icons' },
|
||||
{ pageSlug: 'about', sectionKey: 'team', label: 'Team', description: 'Team members with photos' },
|
||||
{ pageSlug: 'about', sectionKey: 'cta', label: 'Call to Action', description: 'Bottom CTA section' },
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -55,7 +73,16 @@ function defaultTestimonials(): TestimonialsContent {
|
||||
return { title: '', subtitle: '', ratingInfo: '', stats: [], testimonials: [] };
|
||||
}
|
||||
|
||||
function defaultContentFor(sectionKey: string): unknown {
|
||||
function defaultContentFor(pageSlug: string, sectionKey: string): unknown {
|
||||
if (pageSlug === 'about') {
|
||||
switch (sectionKey) {
|
||||
case 'hero': return { badge: '', headline: '', description: '', ctaButtonText: '', bannerImageUrl: '', bannerOverlayText: '' } as AboutHeroContent;
|
||||
case 'stats': return { stats: [] } as AboutStatsContent;
|
||||
case 'features': return { badge: '', features: [] } as AboutFeaturesContent;
|
||||
case 'team': return { title: '', subtitle: '', members: [] } as AboutTeamContent;
|
||||
case 'cta': return { title: '', description: '', buttonText: '' } as AboutCtaContent;
|
||||
}
|
||||
}
|
||||
switch (sectionKey) {
|
||||
case 'hero': return defaultHero();
|
||||
case 'features': return defaultFeatures();
|
||||
@@ -93,17 +120,20 @@ const textareaCls = inputCls;
|
||||
|
||||
export default function CmsPage() {
|
||||
const router = useRouter();
|
||||
const [activePage, setActivePage] = useState<'landing' | 'about'>('landing');
|
||||
const [records, setRecords] = useState<CmsContentRecord[]>([]);
|
||||
const [aboutRecords, setAboutRecords] = useState<CmsContentRecord[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
|
||||
|
||||
// Modal
|
||||
const [editingSection, setEditingSection] = useState<string | null>(null);
|
||||
const [editingSection, setEditingSection] = useState<SectionMeta | null>(null);
|
||||
const [editContent, setEditContent] = useState<unknown>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [modalError, setModalError] = useState('');
|
||||
const [professionalsTab, setProfessionalsTab] = useState<'agents' | 'lenders'>('agents');
|
||||
const [isUploadingImage, setIsUploadingImage] = useState(false);
|
||||
|
||||
// Agent search
|
||||
const [agentSearchQuery, setAgentSearchQuery] = useState('');
|
||||
@@ -113,13 +143,17 @@ export default function CmsPage() {
|
||||
const agentSearchTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const agentSearchRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Fetch all CMS records for "landing" page
|
||||
// Fetch all CMS records
|
||||
const fetchRecords = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const data = await cmsService.getByPage('landing');
|
||||
setRecords(data);
|
||||
const [landingData, aboutData] = await Promise.all([
|
||||
cmsService.getByPage('landing'),
|
||||
cmsService.getByPage('about'),
|
||||
]);
|
||||
setRecords(landingData);
|
||||
setAboutRecords(aboutData);
|
||||
} catch (err) {
|
||||
const msg = getErrorMessage(err);
|
||||
setError(msg);
|
||||
@@ -195,20 +229,21 @@ export default function CmsPage() {
|
||||
}
|
||||
|
||||
// Helpers
|
||||
function recordFor(sectionKey: string): CmsContentRecord | undefined {
|
||||
return records.find((r) => r.sectionKey === sectionKey);
|
||||
function recordFor(pageSlug: string, sectionKey: string): CmsContentRecord | undefined {
|
||||
const source = pageSlug === 'about' ? aboutRecords : records;
|
||||
return source.find((r) => r.sectionKey === sectionKey);
|
||||
}
|
||||
|
||||
// Open modal
|
||||
function openEditor(sectionKey: string) {
|
||||
const existing = recordFor(sectionKey);
|
||||
setEditContent(existing ? JSON.parse(JSON.stringify(existing.content)) : defaultContentFor(sectionKey));
|
||||
function openEditor(section: SectionMeta) {
|
||||
const existing = recordFor(section.pageSlug, section.sectionKey);
|
||||
setEditContent(existing ? JSON.parse(JSON.stringify(existing.content)) : defaultContentFor(section.pageSlug, section.sectionKey));
|
||||
setModalError('');
|
||||
setProfessionalsTab('agents');
|
||||
setAgentSearchQuery('');
|
||||
setAgentSearchResults([]);
|
||||
setShowAgentDropdown(false);
|
||||
setEditingSection(sectionKey);
|
||||
setEditingSection(section);
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
@@ -224,8 +259,8 @@ export default function CmsPage() {
|
||||
setModalError('');
|
||||
try {
|
||||
await cmsService.upsert({
|
||||
pageSlug: 'landing',
|
||||
sectionKey: editingSection,
|
||||
pageSlug: editingSection.pageSlug,
|
||||
sectionKey: editingSection.sectionKey,
|
||||
content: editContent,
|
||||
isPublished: true,
|
||||
});
|
||||
@@ -239,6 +274,23 @@ export default function CmsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Generic image upload — returns the presigned download URL
|
||||
async function handleImageUpload(file: File): Promise<string> {
|
||||
setIsUploadingImage(true);
|
||||
setModalError('');
|
||||
try {
|
||||
const { uploadUrl, key } = await uploadService.getPresignedUploadUrl(file.name, file.type, 'properties');
|
||||
await uploadService.uploadFileToS3(uploadUrl, file);
|
||||
const downloadUrl = await uploadService.getPresignedDownloadUrl(key);
|
||||
return downloadUrl;
|
||||
} catch (err) {
|
||||
setModalError(getErrorMessage(err));
|
||||
return '';
|
||||
} finally {
|
||||
setIsUploadingImage(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section-specific form renderers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -582,9 +634,211 @@ export default function CmsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Reusable image upload field
|
||||
function renderImageUploadField(label: string, currentUrl: string, onUrlChange: (url: string) => void) {
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[#00293d] mb-1">{label}</label>
|
||||
{currentUrl && (
|
||||
<div className="mb-3 relative rounded-lg overflow-hidden border border-[#e5e7eb]">
|
||||
<img src={currentUrl} alt="Preview" className="w-full h-48 object-cover" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
|
||||
<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>
|
||||
<input type="text" className={inputCls} value={currentUrl} onChange={(e) => onUrlChange(e.target.value)} placeholder="https://..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- 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>
|
||||
<input type="text" className={inputCls} value={feat.iconPath} onChange={(e) => updateFeature(idx, { iconPath: e.target.value })} placeholder="Icon path (e.g. /assets/icons/icon.svg)" />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
function renderModalContent() {
|
||||
if (!editingSection || editContent == null) return null;
|
||||
switch (editingSection) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
switch (key) {
|
||||
case 'hero': return renderHeroForm();
|
||||
case 'features': return renderFeaturesForm();
|
||||
case 'topProfessionals': return renderTopProfessionalsForm();
|
||||
@@ -597,14 +851,35 @@ export default function CmsPage() {
|
||||
// Render
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const editingSectionMeta = SECTIONS.find((s) => s.sectionKey === editingSection);
|
||||
const currentSections = activePage === 'landing' ? LANDING_SECTIONS : ABOUT_SECTIONS;
|
||||
const currentRecords = activePage === 'landing' ? records : aboutRecords;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Page header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-[#00293d] font-fractul">Content Management</h1>
|
||||
<p className="text-[#666666] font-serif">Manage landing page content sections</p>
|
||||
<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>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Notification toast */}
|
||||
@@ -629,13 +904,13 @@ export default function CmsPage() {
|
||||
) : (
|
||||
/* Section cards grid */
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{SECTIONS.map((section) => {
|
||||
const record = recordFor(section.sectionKey);
|
||||
{currentSections.map((section) => {
|
||||
const record = recordFor(section.pageSlug, section.sectionKey);
|
||||
const hasContent = !!record;
|
||||
const isPublished = record?.isPublished ?? false;
|
||||
|
||||
return (
|
||||
<div key={section.sectionKey} className="bg-white rounded-xl shadow-sm border border-[#e5e7eb]">
|
||||
<div key={`${section.pageSlug}-${section.sectionKey}`} className="bg-white rounded-xl shadow-sm border border-[#e5e7eb]">
|
||||
<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>
|
||||
@@ -663,7 +938,7 @@ export default function CmsPage() {
|
||||
)}
|
||||
<div className="mt-4 flex justify-end">
|
||||
<button
|
||||
onClick={() => openEditor(section.sectionKey)}
|
||||
onClick={() => openEditor(section)}
|
||||
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">
|
||||
@@ -686,9 +961,9 @@ export default function CmsPage() {
|
||||
{/* Modal header */}
|
||||
<div className="px-6 py-4 border-b border-[#e5e7eb] flex-shrink-0">
|
||||
<h3 className="text-lg font-semibold text-[#00293d]">
|
||||
Edit {editingSectionMeta?.label ?? 'Section'}
|
||||
Edit {editingSection.label}
|
||||
</h3>
|
||||
<p className="text-sm text-[#666666]">{editingSectionMeta?.description}</p>
|
||||
<p className="text-sm text-[#666666]">{editingSection.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Modal body - scrollable */}
|
||||
|
||||
Reference in New Issue
Block a user