feat: Add FAQ content management page with functionality to manage categories and items.

This commit is contained in:
pradeepkumar
2026-02-24 03:53:56 +05:30
parent 9bc70078c2
commit a264c24f26
3 changed files with 173 additions and 5 deletions

View File

@@ -24,6 +24,9 @@ import {
AboutTeamContent, AboutTeamContent,
AboutTeamMember, AboutTeamMember,
AboutCtaContent, AboutCtaContent,
FaqContent,
FaqCategoryItem,
FaqItem,
User, User,
} from '@/services'; } from '@/services';
@@ -53,6 +56,10 @@ const ABOUT_SECTIONS: SectionMeta[] = [
{ pageSlug: 'about', sectionKey: 'cta', label: 'Call to Action', description: 'Bottom CTA section' }, { pageSlug: 'about', sectionKey: 'cta', label: 'Call to Action', description: 'Bottom CTA section' },
]; ];
const FAQ_SECTIONS: SectionMeta[] = [
{ pageSlug: 'faq', sectionKey: 'faqContent', label: 'FAQ Content', description: 'Categories, questions and answers' },
];
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Default content factories // Default content factories
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -83,6 +90,11 @@ function defaultContentFor(pageSlug: string, sectionKey: string): unknown {
case 'cta': return { title: '', description: '', buttonText: '' } as AboutCtaContent; case 'cta': return { title: '', description: '', buttonText: '' } as AboutCtaContent;
} }
} }
if (pageSlug === 'faq') {
switch (sectionKey) {
case 'faqContent': return { pageTitle: '', categories: [], faqs: [], supportTitle: '', supportDescription: '' } as FaqContent;
}
}
switch (sectionKey) { switch (sectionKey) {
case 'hero': return defaultHero(); case 'hero': return defaultHero();
case 'features': return defaultFeatures(); case 'features': return defaultFeatures();
@@ -120,9 +132,10 @@ const textareaCls = inputCls;
export default function CmsPage() { export default function CmsPage() {
const router = useRouter(); const router = useRouter();
const [activePage, setActivePage] = useState<'landing' | 'about'>('landing'); const [activePage, setActivePage] = useState<'landing' | 'about' | 'faq'>('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 [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);
@@ -148,12 +161,14 @@ export default function CmsPage() {
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
try { try {
const [landingData, aboutData] = await Promise.all([ const [landingData, aboutData, faqData] = await Promise.all([
cmsService.getByPage('landing'), cmsService.getByPage('landing'),
cmsService.getByPage('about'), cmsService.getByPage('about'),
cmsService.getByPage('faq'),
]); ]);
setRecords(landingData); setRecords(landingData);
setAboutRecords(aboutData); setAboutRecords(aboutData);
setFaqRecords(faqData);
} catch (err) { } catch (err) {
const msg = getErrorMessage(err); const msg = getErrorMessage(err);
setError(msg); setError(msg);
@@ -230,7 +245,7 @@ export default function CmsPage() {
// Helpers // Helpers
function recordFor(pageSlug: string, sectionKey: string): CmsContentRecord | undefined { function recordFor(pageSlug: string, sectionKey: string): CmsContentRecord | undefined {
const source = pageSlug === 'about' ? aboutRecords : records; const source = pageSlug === 'about' ? aboutRecords : pageSlug === 'faq' ? faqRecords : records;
return source.find((r) => r.sectionKey === sectionKey); return source.find((r) => r.sectionKey === sectionKey);
} }
@@ -825,6 +840,123 @@ export default function CmsPage() {
); );
} }
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>
);
}
function renderModalContent() { function renderModalContent() {
if (!editingSection || editContent == null) return null; if (!editingSection || editContent == null) return null;
const key = editingSection.sectionKey; const key = editingSection.sectionKey;
@@ -838,6 +970,11 @@ export default function CmsPage() {
case 'cta': return renderAboutCtaForm(); case 'cta': return renderAboutCtaForm();
} }
} }
if (page === 'faq') {
switch (key) {
case 'faqContent': return renderFaqForm();
}
}
switch (key) { switch (key) {
case 'hero': return renderHeroForm(); case 'hero': return renderHeroForm();
case 'features': return renderFeaturesForm(); case 'features': return renderFeaturesForm();
@@ -851,8 +988,8 @@ export default function CmsPage() {
// Render // Render
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const currentSections = activePage === 'landing' ? LANDING_SECTIONS : ABOUT_SECTIONS; const currentSections = activePage === 'landing' ? LANDING_SECTIONS : activePage === 'about' ? ABOUT_SECTIONS : FAQ_SECTIONS;
const currentRecords = activePage === 'landing' ? records : aboutRecords; const currentRecords = activePage === 'landing' ? records : activePage === 'about' ? aboutRecords : faqRecords;
return ( return (
<div> <div>
@@ -879,6 +1016,13 @@ export default function CmsPage() {
> >
About Us About Us
</button> </button>
<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>
</nav> </nav>
</div> </div>

View File

@@ -106,6 +106,27 @@ export interface AboutCtaContent {
buttonText: string; buttonText: string;
} }
export interface FaqCategoryItem {
id: string;
label: string;
}
export interface FaqItem {
id: number;
icon: string;
question: string;
answer: string;
category: string;
}
export interface FaqContent {
pageTitle: string;
categories: FaqCategoryItem[];
faqs: FaqItem[];
supportTitle: string;
supportDescription: string;
}
export interface CmsContentRecord { export interface CmsContentRecord {
id: string; id: string;
pageSlug: string; pageSlug: string;

View File

@@ -80,4 +80,7 @@ export type {
AboutTeamMember, AboutTeamMember,
AboutTeamContent, AboutTeamContent,
AboutCtaContent, AboutCtaContent,
FaqCategoryItem,
FaqItem,
FaqContent,
} from './cms.service'; } from './cms.service';