import { HELP_CENTER_SEED_ARTICLES, HELP_CENTER_SEED_CATEGORIES } from "~/data/help-center-seed"; import type { ContentBlock } from "~/data/help-center-seed"; export type HelpArticle = { id: string; slug: string; title: string; summary: string; categoryKey: string; category: string; role: "ALL" | "company" | "jobSeeker" | "professional" | "customer" | "platform"; tags: string[]; updatedAt: string; content: ContentBlock[] | string; }; export type HelpCategory = { id: string; key: string; title: string; }; // ── API fetchers ────────────────────────────────────────────────────────────── export async function fetchHelpCenterArticles(input: { role?: string; categoryKey?: string; q?: string; }): Promise { const params = new URLSearchParams(); if (input.role && input.role !== "ALL") params.set("role", input.role); if (input.categoryKey) params.set("category", input.categoryKey); if (input.q) params.set("q", input.q); try { const res = await fetch(`/api/kb/articles?${params.toString()}`); if (!res.ok) return filterArticles(HELP_CENTER_SEED_ARTICLES as HelpArticle[], input); const data = await res.json(); const raw: any[] = Array.isArray(data) ? data : (data.articles ?? []); let items = raw.map(normalizeArticle); // Fallback: when backend search returns sparse/empty data, apply local filtering // so users can still find articles by simple keywords. if (input.q && items.length === 0) { const allRes = await fetch("/api/kb/articles"); if (allRes.ok) { const allData = await allRes.json(); const allRaw: any[] = Array.isArray(allData) ? allData : (allData.articles ?? []); items = allRaw.map(normalizeArticle); } } if (items.length === 0) { items = HELP_CENTER_SEED_ARTICLES as HelpArticle[]; } return filterArticles(items, input); } catch { return filterArticles(HELP_CENTER_SEED_ARTICLES as HelpArticle[], input); } } export async function fetchHelpCenterCategories(): Promise { try { const res = await fetch("/api/kb/categories"); if (!res.ok) return HELP_CENTER_SEED_CATEGORIES; const data = await res.json(); const raw: any[] = Array.isArray(data) ? data : (data.categories ?? []); const mapped = raw.map((c) => ({ id: c.id, key: c.slug ?? c.key, title: c.name ?? c.title, })); return mapped.length > 0 ? mapped : HELP_CENTER_SEED_CATEGORIES; } catch { return HELP_CENTER_SEED_CATEGORIES; } } export async function fetchArticleBySlug(slug: string): Promise { try { const res = await fetch(`/api/kb/articles/${slug}`); if (!res.ok) return (HELP_CENTER_SEED_ARTICLES as HelpArticle[]).find((a) => a.slug === slug) ?? null; const data = await res.json(); const normalized = normalizeArticle(data); if (!normalized.slug) { return (HELP_CENTER_SEED_ARTICLES as HelpArticle[]).find((a) => a.slug === slug) ?? null; } return normalized; } catch { return (HELP_CENTER_SEED_ARTICLES as HelpArticle[]).find((a) => a.slug === slug) ?? null; } } export async function fetchRelatedArticles(input: { article: HelpArticle; limit?: number; }): Promise { try { const res = await fetch("/api/kb/articles"); if (!res.ok) return pickRelated( HELP_CENTER_SEED_ARTICLES as HelpArticle[], input.article, input.limit ?? 4 ); const data = await res.json(); const raw: any[] = Array.isArray(data) ? data : (data.articles ?? []); const all = raw.map(normalizeArticle); if (all.length === 0) return pickRelated( HELP_CENTER_SEED_ARTICLES as HelpArticle[], input.article, input.limit ?? 4 ); return pickRelated(all, input.article, input.limit ?? 4); } catch { return pickRelated(HELP_CENTER_SEED_ARTICLES as HelpArticle[], input.article, input.limit ?? 4); } } // ── Normalizer ──────────────────────────────────────────────────────────────── function normalizeArticle(raw: any): HelpArticle { const content = raw.content ?? raw.body ?? ""; // Handle both string content (from API) and structured content blocks const normalizedContent = typeof content === "string" && content.startsWith("[") ? JSON.parse(content) : Array.isArray(content) ? content : [{ type: "paragraph", text: content }]; return { id: raw.id ?? "", slug: raw.slug ?? "", title: raw.title ?? "", summary: raw.summary ?? (typeof content === "string" ? content.slice(0, 160) : "") ?? "", categoryKey: raw.categoryKey ?? raw.category_key ?? raw.categorySlug ?? "", category: raw.category ?? raw.categoryKey ?? "", role: (raw.role ?? "ALL") as HelpArticle["role"], tags: Array.isArray(raw.tags) ? raw.tags : [], updatedAt: raw.updatedAt ?? raw.updated_at ?? new Date().toISOString(), content: normalizedContent, }; } // ── Legacy sync shims (kept for any remaining call sites) ───────────────────── // These return empty data synchronously — pages should use the async fetch* functions above. export function listHelpCenterArticles(_input: { role?: string; categoryKey?: string; q?: string; }): HelpArticle[] { return []; } export function listHelpCenterCategories(): HelpCategory[] { return []; } export function getArticleBySlug(_slug: string): HelpArticle | null { return null; } function articleMatchesQuery(article: HelpArticle, needle: string): boolean { const haystack = [ article.title, article.summary, article.content, article.category, article.categoryKey, article.tags.join(" "), ] .join(" ") .toLowerCase(); return haystack.includes(needle); } function filterArticles( items: HelpArticle[], input: { role?: string; categoryKey?: string; q?: string } ): HelpArticle[] { let filtered = items; if (input.role && input.role !== "ALL") { const roleNeedle = input.role.toLowerCase(); filtered = filtered.filter( (a) => a.role === "ALL" || String(a.role).toLowerCase() === roleNeedle ); } if (input.categoryKey) { const needle = input.categoryKey.toLowerCase(); filtered = filtered.filter((a) => [a.categoryKey, a.category].some((v) => (v || "").toLowerCase().includes(needle)) ); } if (input.q) { const needle = input.q.toLowerCase(); filtered = filtered.filter((a) => articleMatchesQuery(a, needle)); } return filtered; } function pickRelated(allItems: HelpArticle[], current: HelpArticle, limit: number): HelpArticle[] { const all = allItems.filter((a) => a.slug !== current.slug); const sameCategory = all.filter((a) => { const left = `${a.categoryKey}|${a.category}`.toLowerCase(); const right = `${current.categoryKey}|${current.category}`.toLowerCase(); return left && right && left === right; }); const fallback = all.filter((a) => !sameCategory.some((x) => x.slug === a.slug)); return [...sameCategory, ...fallback].slice(0, limit); }