- Backend: faq_questions table (schema + migration), CRUD + reorder API, Swagger docs - Admin: FAQ page with create/edit, enable/disable, show on homepage, drag reorder - Public /faq page fetches enabled FAQs from API; layout builds dynamic JSON-LD - Homepage: FAQ section under Stay updated (homepage-enabled only) with See full FAQ link - llms.txt: FAQ section uses homepage FAQs from API - i18n: home.faq title/seeFull, admin FAQ nav Co-authored-by: Cursor <cursoragent@cursor.com>
243 lines
8.2 KiB
TypeScript
243 lines
8.2 KiB
TypeScript
import { Hono } from 'hono';
|
||
import { db, dbGet, dbAll, faqQuestions } from '../db/index.js';
|
||
import { eq, asc } from 'drizzle-orm';
|
||
import { requireAuth } from '../lib/auth.js';
|
||
import { getNow, generateId } from '../lib/utils.js';
|
||
|
||
const faqRouter = new Hono();
|
||
|
||
// ==================== Public Routes ====================
|
||
|
||
// Get FAQ list for public (only enabled; optional filter for homepage)
|
||
faqRouter.get('/', async (c) => {
|
||
const homepage = c.req.query('homepage') === 'true';
|
||
|
||
let query = (db as any)
|
||
.select()
|
||
.from(faqQuestions)
|
||
.where(eq((faqQuestions as any).enabled, 1))
|
||
.orderBy(asc((faqQuestions as any).rank), asc((faqQuestions as any).createdAt));
|
||
|
||
const rows = await dbAll<any>(query);
|
||
|
||
let items = rows;
|
||
if (homepage) {
|
||
items = rows.filter((r: any) => r.showOnHomepage === true || r.showOnHomepage === 1 || r.show_on_homepage === true || r.show_on_homepage === 1);
|
||
}
|
||
|
||
return c.json({
|
||
faqs: items.map((r: any) => ({
|
||
id: r.id,
|
||
question: r.question,
|
||
questionEs: r.questionEs ?? r.question_es ?? null,
|
||
answer: r.answer,
|
||
answerEs: r.answerEs ?? r.answer_es ?? null,
|
||
rank: r.rank ?? 0,
|
||
})),
|
||
});
|
||
});
|
||
|
||
// ==================== Admin Routes ====================
|
||
|
||
// Get all FAQ questions for admin (all, ordered by rank)
|
||
faqRouter.get('/admin/list', requireAuth(['admin']), async (c) => {
|
||
const rows = await dbAll<any>(
|
||
(db as any)
|
||
.select()
|
||
.from(faqQuestions)
|
||
.orderBy(asc((faqQuestions as any).rank), asc((faqQuestions as any).createdAt))
|
||
);
|
||
|
||
const list = rows.map((r: any) => ({
|
||
id: r.id,
|
||
question: r.question,
|
||
questionEs: r.questionEs ?? r.question_es ?? null,
|
||
answer: r.answer,
|
||
answerEs: r.answerEs ?? r.answer_es ?? null,
|
||
enabled: r.enabled === true || r.enabled === 1,
|
||
showOnHomepage: r.showOnHomepage === true || r.showOnHomepage === 1 || r.show_on_homepage === true || r.show_on_homepage === 1,
|
||
rank: r.rank ?? 0,
|
||
createdAt: r.createdAt,
|
||
updatedAt: r.updatedAt,
|
||
}));
|
||
|
||
return c.json({ faqs: list });
|
||
});
|
||
|
||
// Get one FAQ by id (admin)
|
||
faqRouter.get('/admin/:id', requireAuth(['admin']), async (c) => {
|
||
const { id } = c.req.param();
|
||
const row = await dbGet<any>(
|
||
(db as any).select().from(faqQuestions).where(eq((faqQuestions as any).id, id))
|
||
);
|
||
if (!row) {
|
||
return c.json({ error: 'FAQ not found' }, 404);
|
||
}
|
||
return c.json({
|
||
faq: {
|
||
id: row.id,
|
||
question: row.question,
|
||
questionEs: row.questionEs ?? row.question_es ?? null,
|
||
answer: row.answer,
|
||
answerEs: row.answerEs ?? row.answer_es ?? null,
|
||
enabled: row.enabled === true || row.enabled === 1,
|
||
showOnHomepage: row.showOnHomepage === true || row.showOnHomepage === 1 || row.show_on_homepage === true || row.show_on_homepage === 1,
|
||
rank: row.rank ?? 0,
|
||
createdAt: row.createdAt,
|
||
updatedAt: row.updatedAt,
|
||
},
|
||
});
|
||
});
|
||
|
||
// Create FAQ (admin)
|
||
faqRouter.post('/admin', requireAuth(['admin']), async (c) => {
|
||
const body = await c.req.json();
|
||
const { question, questionEs, answer, answerEs, enabled, showOnHomepage } = body;
|
||
|
||
if (!question || typeof question !== 'string' || !answer || typeof answer !== 'string') {
|
||
return c.json({ error: 'Question and answer (EN) are required' }, 400);
|
||
}
|
||
|
||
const now = getNow();
|
||
const id = generateId();
|
||
|
||
const allForRank = await dbAll<any>(
|
||
(db as any).select({ rank: (faqQuestions as any).rank }).from(faqQuestions)
|
||
);
|
||
const maxRank = allForRank.length
|
||
? Math.max(...allForRank.map((r: any) => Number(r.rank ?? 0)))
|
||
: 0;
|
||
const nextRank = maxRank + 1;
|
||
|
||
await (db as any).insert(faqQuestions).values({
|
||
id,
|
||
question: String(question).trim(),
|
||
questionEs: questionEs != null ? String(questionEs).trim() : null,
|
||
answer: String(answer).trim(),
|
||
answerEs: answerEs != null ? String(answerEs).trim() : null,
|
||
enabled: enabled !== false ? 1 : 0,
|
||
showOnHomepage: showOnHomepage === true ? 1 : 0,
|
||
rank: nextRank,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
});
|
||
|
||
const created = await dbGet<any>(
|
||
(db as any).select().from(faqQuestions).where(eq((faqQuestions as any).id, id))
|
||
);
|
||
return c.json(
|
||
{
|
||
faq: {
|
||
id: created.id,
|
||
question: created.question,
|
||
questionEs: created.questionEs ?? created.question_es ?? null,
|
||
answer: created.answer,
|
||
answerEs: created.answerEs ?? created.answer_es ?? null,
|
||
enabled: created.enabled === true || created.enabled === 1,
|
||
showOnHomepage: created.showOnHomepage === true || created.showOnHomepage === 1 || created.show_on_homepage === true || created.show_on_homepage === 1,
|
||
rank: created.rank ?? 0,
|
||
createdAt: created.createdAt,
|
||
updatedAt: created.updatedAt,
|
||
},
|
||
},
|
||
201
|
||
);
|
||
});
|
||
|
||
// Update FAQ (admin)
|
||
faqRouter.put('/admin/:id', requireAuth(['admin']), async (c) => {
|
||
const { id } = c.req.param();
|
||
const body = await c.req.json();
|
||
const { question, questionEs, answer, answerEs, enabled, showOnHomepage } = body;
|
||
|
||
const existing = await dbGet<any>(
|
||
(db as any).select().from(faqQuestions).where(eq((faqQuestions as any).id, id))
|
||
);
|
||
if (!existing) {
|
||
return c.json({ error: 'FAQ not found' }, 404);
|
||
}
|
||
|
||
const updateData: Record<string, unknown> = {
|
||
updatedAt: getNow(),
|
||
};
|
||
if (question !== undefined) updateData.question = String(question).trim();
|
||
if (questionEs !== undefined) updateData.questionEs = questionEs == null ? null : String(questionEs).trim();
|
||
if (answer !== undefined) updateData.answer = String(answer).trim();
|
||
if (answerEs !== undefined) updateData.answerEs = answerEs == null ? null : String(answerEs).trim();
|
||
if (typeof enabled === 'boolean') updateData.enabled = enabled ? 1 : 0;
|
||
if (typeof showOnHomepage === 'boolean') updateData.showOnHomepage = showOnHomepage ? 1 : 0;
|
||
|
||
await (db as any).update(faqQuestions).set(updateData).where(eq((faqQuestions as any).id, id));
|
||
|
||
const updated = await dbGet<any>(
|
||
(db as any).select().from(faqQuestions).where(eq((faqQuestions as any).id, id))
|
||
);
|
||
return c.json({
|
||
faq: {
|
||
id: updated.id,
|
||
question: updated.question,
|
||
questionEs: updated.questionEs ?? updated.question_es ?? null,
|
||
answer: updated.answer,
|
||
answerEs: updated.answerEs ?? updated.answer_es ?? null,
|
||
enabled: updated.enabled === true || updated.enabled === 1,
|
||
showOnHomepage: updated.showOnHomepage === true || updated.showOnHomepage === 1 || updated.show_on_homepage === true || updated.show_on_homepage === 1,
|
||
rank: updated.rank ?? 0,
|
||
createdAt: updated.createdAt,
|
||
updatedAt: updated.updatedAt,
|
||
},
|
||
});
|
||
});
|
||
|
||
// Delete FAQ (admin)
|
||
faqRouter.delete('/admin/:id', requireAuth(['admin']), async (c) => {
|
||
const { id } = c.req.param();
|
||
const existing = await dbGet<any>(
|
||
(db as any).select().from(faqQuestions).where(eq((faqQuestions as any).id, id))
|
||
);
|
||
if (!existing) {
|
||
return c.json({ error: 'FAQ not found' }, 404);
|
||
}
|
||
await (db as any).delete(faqQuestions).where(eq((faqQuestions as any).id, id));
|
||
return c.json({ message: 'FAQ deleted' });
|
||
});
|
||
|
||
// Reorder FAQs (admin) – body: { ids: string[] } (ordered list of ids)
|
||
faqRouter.post('/admin/reorder', requireAuth(['admin']), async (c) => {
|
||
const body = await c.req.json();
|
||
const { ids } = body;
|
||
if (!Array.isArray(ids) || ids.length === 0) {
|
||
return c.json({ error: 'ids array is required' }, 400);
|
||
}
|
||
|
||
const now = getNow();
|
||
for (let i = 0; i < ids.length; i++) {
|
||
await (db as any)
|
||
.update(faqQuestions)
|
||
.set({ rank: i, updatedAt: now })
|
||
.where(eq((faqQuestions as any).id, ids[i]));
|
||
}
|
||
|
||
const rows = await dbAll<any>(
|
||
(db as any)
|
||
.select()
|
||
.from(faqQuestions)
|
||
.orderBy(asc((faqQuestions as any).rank), asc((faqQuestions as any).createdAt))
|
||
);
|
||
const list = rows.map((r: any) => ({
|
||
id: r.id,
|
||
question: r.question,
|
||
questionEs: r.questionEs ?? r.question_es ?? null,
|
||
answer: r.answer,
|
||
answerEs: r.answerEs ?? r.answer_es ?? null,
|
||
enabled: r.enabled === true || r.enabled === 1,
|
||
showOnHomepage: r.showOnHomepage === true || r.showOnHomepage === 1 || r.show_on_homepage === true || r.show_on_homepage === 1,
|
||
rank: r.rank ?? 0,
|
||
createdAt: r.createdAt,
|
||
updatedAt: r.updatedAt,
|
||
}));
|
||
|
||
return c.json({ faqs: list });
|
||
});
|
||
|
||
export default faqRouter;
|