From 3209d130118df454bd9f2ad14515404ceeac21b1 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Thu, 2 Apr 2026 13:36:16 +0200 Subject: [PATCH] feat: wire help center and article pages to live KB API - help-center.ts: replace static HELP_ARTICLES array with async fetch* functions (fetchHelpCenterArticles, fetchHelpCenterCategories, fetchArticleBySlug); legacy sync shims kept for safety - support/index.tsx: switched from createMemo(static) to createResource(async API) with loading states - help-center/article/[slug].tsx: now fetches article from API via createResource; renders paragraphs split by double-newline; proper loading and not-found states - New server-side API routes: /api/kb/articles, /api/kb/categories, /api/kb/articles/[slug] (proxy to Rust gateway, no auth required) Co-Authored-By: Claude Sonnet 4.6 --- src/lib/help-center.ts | 145 ++++++++++++---------- src/routes/api/kb/articles.ts | 19 +++ src/routes/api/kb/articles/[slug].ts | 18 +++ src/routes/api/kb/categories.ts | 18 +++ src/routes/help-center/article/[slug].tsx | 110 +++++++++------- src/routes/support/index.tsx | 139 +++++++++++++-------- 6 files changed, 287 insertions(+), 162 deletions(-) create mode 100644 src/routes/api/kb/articles.ts create mode 100644 src/routes/api/kb/articles/[slug].ts create mode 100644 src/routes/api/kb/categories.ts diff --git a/src/lib/help-center.ts b/src/lib/help-center.ts index 88f5454..18ba638 100644 --- a/src/lib/help-center.ts +++ b/src/lib/help-center.ts @@ -4,80 +4,97 @@ export type HelpArticle = { title: string; summary: string; categoryKey: string; + category: string; role: 'ALL' | 'company' | 'jobSeeker' | 'professional' | 'customer' | 'platform'; tags: string[]; updatedAt: string; content: string; }; -export const HELP_ARTICLES: HelpArticle[] = [ - { - id: 'hc-1', - slug: 'how-verification-works', - title: 'How verification works', - summary: 'Understand document review steps, approval outcomes, and timeline.', - categoryKey: 'verification', - role: 'ALL', - tags: ['verification', 'documents', 'approval'], - updatedAt: '2026-03-17T00:00:00Z', - content: 'After signup, complete onboarding for one path and submit required documents. Admin review updates your status as pending, document required, approved, or rejected.', - }, - { - id: 'hc-2', - slug: 'customer-post-requirement', - title: 'How customers post requirements', - summary: 'Choose profession intent, add requirements, and track verified responses.', - categoryKey: 'requirements', - role: 'customer', - tags: ['customer', 'requirements'], - updatedAt: '2026-03-17T00:00:00Z', - content: 'Customer flow starts with selecting the professional category, then requirement details, budget, and timeline. After review, qualified professionals can respond.', - }, - { - id: 'hc-3', - slug: 'professional-onboarding-guide', - title: 'Professional onboarding guide', - summary: 'Choose your profession, upload portfolio, submit PDF ID documents, and wait for approval.', - categoryKey: 'onboarding', - role: 'professional', - tags: ['professional', 'onboarding', 'portfolio'], - updatedAt: '2026-03-17T00:00:00Z', - content: 'Each profession in Solid has its own onboarding and service configuration. Complete all steps and verification to unlock your full dashboard.', - }, -]; +export type HelpCategory = { + id: string; + key: string; + title: string; +}; -export function listHelpCenterArticles(input: { role?: string; categoryKey?: string; q?: string }) { - const role = String(input.role || 'ALL'); - const categoryKey = String(input.categoryKey || '').trim(); - const q = String(input.q || '').trim().toLowerCase(); +// ── API fetchers ────────────────────────────────────────────────────────────── - return HELP_ARTICLES.filter((article) => { - const roleOk = role === 'ALL' || article.role === 'ALL' || article.role === role; - const categoryOk = !categoryKey || article.categoryKey === categoryKey; - const queryOk = !q || `${article.title} ${article.summary} ${article.tags.join(' ')}`.toLowerCase().includes(q); - return roleOk && categoryOk && queryOk; - }); -} +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); -export function listHelpCenterCategories() { - const keys = new Map(); - for (const article of HELP_ARTICLES) { - if (!keys.has(article.categoryKey)) { - const title = article.categoryKey - .split('-') - .map((chunk) => chunk.charAt(0).toUpperCase() + chunk.slice(1)) - .join(' '); - keys.set(article.categoryKey, title); - } + try { + const res = await fetch(`/api/kb/articles?${params.toString()}`); + if (!res.ok) return []; + const data = await res.json(); + const raw: any[] = Array.isArray(data) ? data : (data.articles ?? []); + return raw.map(normalizeArticle); + } catch { + return []; } - - return Array.from(keys.entries()).map(([key, title], idx) => ({ - id: `cat-${idx + 1}`, - key, - title, - })); } -export function getArticleBySlug(slug: string) { - return HELP_ARTICLES.find((article) => article.slug === slug) || null; +export async function fetchHelpCenterCategories(): Promise { + try { + const res = await fetch('/api/kb/categories'); + if (!res.ok) return []; + const data = await res.json(); + const raw: any[] = Array.isArray(data) ? data : (data.categories ?? []); + return raw.map((c) => ({ + id: c.id, + key: c.slug, + title: c.name, + })); + } catch { + return []; + } +} + +export async function fetchArticleBySlug(slug: string): Promise { + try { + const res = await fetch(`/api/kb/articles/${slug}`); + if (!res.ok) return null; + const data = await res.json(); + return normalizeArticle(data); + } catch { + return null; + } +} + +// ── Normalizer ──────────────────────────────────────────────────────────────── + +function normalizeArticle(raw: any): HelpArticle { + return { + id: raw.id ?? '', + slug: raw.slug ?? '', + title: raw.title ?? '', + summary: raw.summary ?? raw.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: raw.content ?? raw.body ?? '', + }; +} + +// ── 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; } diff --git a/src/routes/api/kb/articles.ts b/src/routes/api/kb/articles.ts new file mode 100644 index 0000000..b35e937 --- /dev/null +++ b/src/routes/api/kb/articles.ts @@ -0,0 +1,19 @@ +import { gatewayUrl } from '~/lib/server/gateway'; + +export async function GET({ request }: { request: Request }) { + const url = new URL(request.url); + const upstream = gatewayUrl('/api/kb/articles' + url.search); + try { + const res = await fetch(upstream, { cache: 'no-store' }); + const body = await res.text(); + return new Response(body, { + status: res.status, + headers: { 'Content-Type': 'application/json' }, + }); + } catch (err: any) { + return new Response(JSON.stringify({ error: err?.message || 'Gateway error' }), { + status: 502, + headers: { 'Content-Type': 'application/json' }, + }); + } +} diff --git a/src/routes/api/kb/articles/[slug].ts b/src/routes/api/kb/articles/[slug].ts new file mode 100644 index 0000000..41e416f --- /dev/null +++ b/src/routes/api/kb/articles/[slug].ts @@ -0,0 +1,18 @@ +import { gatewayUrl } from '~/lib/server/gateway'; + +export async function GET({ params }: { params: { slug: string } }) { + const upstream = gatewayUrl(`/api/kb/articles/${params.slug}`); + try { + const res = await fetch(upstream, { cache: 'no-store' }); + const body = await res.text(); + return new Response(body, { + status: res.status, + headers: { 'Content-Type': 'application/json' }, + }); + } catch (err: any) { + return new Response(JSON.stringify({ error: err?.message || 'Gateway error' }), { + status: 502, + headers: { 'Content-Type': 'application/json' }, + }); + } +} diff --git a/src/routes/api/kb/categories.ts b/src/routes/api/kb/categories.ts new file mode 100644 index 0000000..9b64a4f --- /dev/null +++ b/src/routes/api/kb/categories.ts @@ -0,0 +1,18 @@ +import { gatewayUrl } from '~/lib/server/gateway'; + +export async function GET() { + const upstream = gatewayUrl('/api/kb/categories'); + try { + const res = await fetch(upstream, { cache: 'no-store' }); + const body = await res.text(); + return new Response(body, { + status: res.status, + headers: { 'Content-Type': 'application/json' }, + }); + } catch (err: any) { + return new Response(JSON.stringify({ error: err?.message || 'Gateway error' }), { + status: 502, + headers: { 'Content-Type': 'application/json' }, + }); + } +} diff --git a/src/routes/help-center/article/[slug].tsx b/src/routes/help-center/article/[slug].tsx index bd4acf8..c2b8eb3 100644 --- a/src/routes/help-center/article/[slug].tsx +++ b/src/routes/help-center/article/[slug].tsx @@ -1,6 +1,6 @@ import { A, useParams } from '@solidjs/router'; -import { createSignal, onCleanup, onMount } from 'solid-js'; -import { getArticleBySlug } from '~/lib/help-center'; +import { Show, For, createSignal, createResource, onCleanup, onMount } from 'solid-js'; +import { fetchArticleBySlug } from '~/lib/help-center'; import PublicBackground from '~/components/PublicBackground'; import PublicHeader from '~/components/PublicHeader'; import PublicFooter from '~/components/PublicFooter'; @@ -15,9 +15,10 @@ function categoryTitle(input: string) { export default function HelpCenterArticlePage() { const params = useParams(); - const article = getArticleBySlug(params.slug || ''); const [scrollY, setScrollY] = createSignal(0); + const [article] = createResource(() => params.slug, fetchArticleBySlug); + onMount(() => { const onScroll = () => setScrollY(window.scrollY || 0); onScroll(); @@ -25,12 +26,21 @@ export default function HelpCenterArticlePage() { onCleanup(() => window.removeEventListener('scroll', onScroll)); }); - if (!article) { - return ( -
- -
- + return ( +
+ +
+ + + +
+
+

Loading article…

+
+
+
+ +

Article not found

@@ -40,49 +50,55 @@ export default function HelpCenterArticlePage() {
- -
-
- ); - } + - return ( -
- -
- -
-
-

{categoryTitle(article.categoryKey)}

-

{article.title}

-

{article.summary}

+ + {(a) => ( + <> +
+
+

{a().category || categoryTitle(a().categoryKey)}

+

{a().title}

+

{a().summary}

- -

Updated {new Date(article.updatedAt).toLocaleDateString()}

+ 0}> + + -
-

{article.content}

-
+

Updated {new Date(a().updatedAt).toLocaleDateString()}

- -
-
+
+ + {(para) =>

{para}

} +
+
-
-
-

Need more help?

-

If this article does not solve your issue, send your question with context to support.

- -
-
+ +
+
+ +
+
+

Need more help?

+

+ If this article does not solve your issue, send your question with context to support. +

+ +
+
+ + )} +
diff --git a/src/routes/support/index.tsx b/src/routes/support/index.tsx index 04ac773..777586d 100644 --- a/src/routes/support/index.tsx +++ b/src/routes/support/index.tsx @@ -1,6 +1,6 @@ import { A, useSearchParams } from '@solidjs/router'; -import { For, createMemo, createSignal, onCleanup, onMount } from 'solid-js'; -import { listHelpCenterArticles, listHelpCenterCategories } from '~/lib/help-center'; +import { For, Show, createMemo, createResource, createSignal, onCleanup, onMount } from 'solid-js'; +import { fetchHelpCenterArticles, fetchHelpCenterCategories } from '~/lib/help-center'; import PublicBackground from '~/components/PublicBackground'; import PublicHeader from '~/components/PublicHeader'; import PublicFooter from '~/components/PublicFooter'; @@ -30,22 +30,38 @@ export default function SupportPage() { const category = createMemo(() => String(search.category || '')); const q = createMemo(() => String(search.q || '')); - const categories = createMemo(() => listHelpCenterCategories()); - const articles = createMemo(() => - listHelpCenterArticles({ role: role(), categoryKey: category() || undefined, q: q() || undefined }), - ); + const [categories] = createResource(fetchHelpCenterCategories); + + const articleParams = createMemo(() => ({ + role: role(), + categoryKey: category() || undefined, + q: q() || undefined, + })); + + const [articles] = createResource(articleParams, (p) => fetchHelpCenterArticles(p)); + const visibleCategories = createMemo(() => { - if (categories().length > 0) return categories(); + const cats = categories(); + if (cats && cats.length > 0) return cats; + const arts = articles(); + if (!arts) return []; const seen = new Set(); - return articles() + return arts .filter((item) => { if (seen.has(item.categoryKey)) return false; seen.add(item.categoryKey); return true; }) - .map((item, idx) => ({ id: `derived-${idx + 1}`, key: item.categoryKey, title: categoryTitle(item.categoryKey) })); + .map((item, idx) => ({ + id: `derived-${idx + 1}`, + key: item.categoryKey, + title: item.category || categoryTitle(item.categoryKey), + })); }); - const categoryName = createMemo(() => visibleCategories().find((cat) => cat.key === category())?.title || categoryTitle(category())); + + const categoryName = createMemo( + () => visibleCategories().find((cat) => cat.key === category())?.title || categoryTitle(category()), + ); onMount(() => { const onScroll = () => setScrollY(window.scrollY || 0); @@ -66,7 +82,7 @@ export default function SupportPage() {

Help Center

Get answers quickly

- Articles are loaded at runtime from published Help Center management content so public users always see the latest approved guidance. + Browse articles by role or category, or search for what you need.

@@ -82,28 +98,33 @@ export default function SupportPage() {

Categories

- {category() && ( + Clear category filter - )} +
-
- - {(cat) => ( - - {cat.title} - - )} - -
+ +
Loading categories…
+
+ +
+ + {(cat) => ( + + {cat.title} + + )} + +
+
@@ -117,32 +138,44 @@ export default function SupportPage() { ? 'Latest articles' : `${ROLE_LABELS[role()] || 'Role'} articles`} - {articles().length} articles + {articles()?.length ?? 0} articles -
- - {(article) => ( -
-

{categoryTitle(article.categoryKey)}

-

- {article.title} -

-

{article.summary}

- - -
- )} -
- {articles().length === 0 &&
No Help Center articles matched your filters.
} -
+ +
Loading articles…
+
+ + +
+ + {(article) => ( +
+

{article.category || categoryTitle(article.categoryKey)}

+

+ + {article.title} + +

+

{article.summary}

+ + +
+ )} +
+ +
No Help Center articles matched your filters.
+
+
+
@@ -151,10 +184,14 @@ export default function SupportPage() {

Still have questions?

Ask the support team

-

Share your role, what you tried, and which article you checked so support can respond faster.

+

+ Share your role, what you tried, and which article you checked so support can respond faster. +