feat: Add FAQ content management page with functionality to manage categories and items.
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -80,4 +80,7 @@ export type {
|
|||||||
AboutTeamMember,
|
AboutTeamMember,
|
||||||
AboutTeamContent,
|
AboutTeamContent,
|
||||||
AboutCtaContent,
|
AboutCtaContent,
|
||||||
|
FaqCategoryItem,
|
||||||
|
FaqItem,
|
||||||
|
FaqContent,
|
||||||
} from './cms.service';
|
} from './cms.service';
|
||||||
|
|||||||
Reference in New Issue
Block a user