feat: member portal Sprint 10 — Knowledge Base

- KnowledgeBoard + KnowledgeItem models + migration (DRAFT/PUBLISHED/ARCHIVED status, slug per tenant)
- KnowledgeModule: admin CRUD for boards + items (auto-slug, status workflow)
- Member endpoint GET /my/knowledge?q= — published items grouped by board, optional text search over question/answer
- /my/knowledge page: search box + collapsible boards with accordion FAQ items
- KnowledgeItem accordion + KnowledgeSearch (debounced URL-driven) client components
- Knowledge nav item; register KnowledgeModule

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 17:06:26 +05:30
parent f61c070428
commit 5801cbf85b
13 changed files with 479 additions and 1 deletions
+66
View File
@@ -0,0 +1,66 @@
import { getMemberToken, getApiBaseUrl } from '../../_lib/api';
import { KnowledgeItem } from './KnowledgeItem';
import { KnowledgeSearch } from './KnowledgeSearch';
import Link from 'next/link';
interface Board {
id: string;
name: string;
description: string | null;
items: Array<{ id: string; question: string; answer: string; tags: string[] }>;
}
async function fetchKnowledge(token: string, q?: string): Promise<Board[]> {
const params = new URLSearchParams();
if (q) params.set('q', q);
const res = await fetch(`${getApiBaseUrl()}/my/knowledge${params.toString() ? `?${params.toString()}` : ''}`, {
headers: { Authorization: `Bearer ${token}` },
cache: 'no-store',
}).catch(() => null);
if (!res || !res.ok) return [];
return (await res.json()) as Board[];
}
export default async function KnowledgePage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const token = await getMemberToken();
if (!token) return null;
const { q } = await searchParams;
const boards = await fetchKnowledge(token, q);
return (
<div className="max-w-2xl mx-auto space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold text-gray-900">Knowledge base</h1>
<Link href="/my" className="text-sm text-indigo-600 hover:underline"> Dashboard</Link>
</div>
<KnowledgeSearch />
{boards.length === 0 ? (
<div className="text-center py-16 text-gray-400">
<p className="text-sm">{q ? 'No matching articles.' : 'No knowledge articles yet.'}</p>
{!q && <p className="text-xs mt-1">Your chapter admin will publish FAQs and guides here.</p>}
</div>
) : (
<div className="space-y-5">
{boards.map((b) => (
<section key={b.id} className="bg-white rounded-xl border border-gray-200 p-5">
<h2 className="text-sm font-semibold text-gray-900">{b.name}</h2>
{b.description && <p className="text-xs text-gray-400 mt-0.5 mb-1">{b.description}</p>}
<div className="mt-1">
{b.items.map((item) => (
<KnowledgeItem key={item.id} question={item.question} answer={item.answer} tags={item.tags} />
))}
</div>
</section>
))}
</div>
)}
</div>
);
}