feat: improve Help Center UI with mixed dark/light theme and structured content
- Updated Help Center with dark hero and light content sections - Added ArticleContent component for rendering structured content blocks - Updated seed data with detailed articles matching admin KB categories - Fixed article alignment and spacing issues - Uses ContentBlock[] instead of HTML strings for type-safe content
This commit is contained in:
parent
01d37fb109
commit
7671ad8e55
7 changed files with 1875 additions and 524 deletions
137
src/components/ArticleContent.tsx
Normal file
137
src/components/ArticleContent.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { For, Show } from "solid-js";
|
||||
import type { ContentBlock } from "~/data/help-center-seed";
|
||||
|
||||
interface ArticleContentProps {
|
||||
blocks: ContentBlock[];
|
||||
}
|
||||
|
||||
export default function ArticleContent(props: ArticleContentProps) {
|
||||
const renderBlock = (block: ContentBlock) => {
|
||||
switch (block.type) {
|
||||
case "paragraph":
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
margin: "0 0 16px",
|
||||
color: "#374151",
|
||||
"font-size": "16px",
|
||||
"line-height": "1.8",
|
||||
}}
|
||||
>
|
||||
{block.text}
|
||||
</p>
|
||||
);
|
||||
|
||||
case "heading":
|
||||
if (block.level === 2) {
|
||||
return (
|
||||
<h2
|
||||
style={{
|
||||
margin: "32px 0 16px",
|
||||
color: "#111827",
|
||||
"font-size": "24px",
|
||||
"font-weight": "700",
|
||||
"border-bottom": "1px solid #E5E7EB",
|
||||
"padding-bottom": "8px",
|
||||
}}
|
||||
>
|
||||
{block.text}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<h3
|
||||
style={{
|
||||
margin: "24px 0 12px",
|
||||
color: "#1F2937",
|
||||
"font-size": "18px",
|
||||
"font-weight": "600",
|
||||
}}
|
||||
>
|
||||
{block.text}
|
||||
</h3>
|
||||
);
|
||||
|
||||
case "list":
|
||||
if (block.ordered) {
|
||||
return (
|
||||
<ol
|
||||
style={{
|
||||
margin: "0 0 20px",
|
||||
padding: "0 0 0 24px",
|
||||
color: "#374151",
|
||||
"font-size": "16px",
|
||||
"line-height": "1.8",
|
||||
}}
|
||||
>
|
||||
<For each={block.items}>
|
||||
{(item) => (
|
||||
<li style={{ margin: "8px 0" }}>
|
||||
<strong style={{ color: "#111827" }}>{item.split(":")[0]}:</strong>
|
||||
{item.split(":")[1] || ""}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ul
|
||||
style={{
|
||||
margin: "0 0 20px",
|
||||
padding: "0 0 0 24px",
|
||||
color: "#374151",
|
||||
"font-size": "16px",
|
||||
"line-height": "1.8",
|
||||
"list-style-type": "disc",
|
||||
}}
|
||||
>
|
||||
<For each={block.items}>
|
||||
{(item) => (
|
||||
<li style={{ margin: "8px 0" }}>
|
||||
<strong style={{ color: "#111827" }}>{item.split(":")[0]}:</strong>
|
||||
{item.split(":")[1] || ""}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
);
|
||||
|
||||
case "section":
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
margin: "24px 0",
|
||||
padding: "20px",
|
||||
background: "#F9FAFB",
|
||||
"border-radius": "12px",
|
||||
border: "1px solid #E5E7EB",
|
||||
}}
|
||||
>
|
||||
<h4
|
||||
style={{
|
||||
margin: "0 0 16px",
|
||||
color: "#EA580C",
|
||||
"font-size": "14px",
|
||||
"font-weight": "700",
|
||||
"text-transform": "uppercase",
|
||||
"letter-spacing": "0.5px",
|
||||
}}
|
||||
>
|
||||
{block.title}
|
||||
</h4>
|
||||
<For each={block.blocks}>{(subBlock) => renderBlock(subBlock)}</For>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<article style={{ color: "#374151" }}>
|
||||
<For each={props.blocks}>{(block) => renderBlock(block)}</For>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,30 +1,37 @@
|
|||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||
import { BTN_GHOST, CARD, INPUT } from '~/components/DashboardShell';
|
||||
import { type RoleKey } from './RoleDashboardShared';
|
||||
import { For, Show, createMemo, createSignal, onMount } from "solid-js";
|
||||
import { BTN_GHOST, CARD, INPUT } from "~/components/DashboardShell";
|
||||
import { type RoleKey } from "./RoleDashboardShared";
|
||||
|
||||
const API = '/api/gateway';
|
||||
const API = "/api/gateway";
|
||||
|
||||
type Props = { roleKey: RoleKey };
|
||||
|
||||
type Category = { id: string; name: string; slug: string; description?: string };
|
||||
type Article = { id: string; title: string; slug: string; summary?: string; category?: string; updatedAt?: string };
|
||||
type Article = {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
summary?: string;
|
||||
category?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
async function apiFetch(path: string, opts?: RequestInit) {
|
||||
return fetch(`${API}${path}`, {
|
||||
...opts,
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", ...(opts?.headers ?? {}) },
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeArticle(raw: any): Article {
|
||||
return {
|
||||
id: String(raw?.id || ''),
|
||||
title: String(raw?.title || ''),
|
||||
slug: String(raw?.slug || ''),
|
||||
summary: raw?.summary ? String(raw.summary) : '',
|
||||
category: String(raw?.category || raw?.category_name || ''),
|
||||
updatedAt: String(raw?.updatedAt || raw?.updated_at || ''),
|
||||
id: String(raw?.id || ""),
|
||||
title: String(raw?.title || ""),
|
||||
slug: String(raw?.slug || ""),
|
||||
summary: raw?.summary ? String(raw.summary) : "",
|
||||
category: String(raw?.category || raw?.category_name || ""),
|
||||
updatedAt: String(raw?.updatedAt || raw?.updated_at || ""),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -32,15 +39,15 @@ export default function HelpCenterDashboardPage(props: Props) {
|
|||
const [categories, setCategories] = createSignal<Category[]>([]);
|
||||
const [articles, setArticles] = createSignal<Article[]>([]);
|
||||
const [loading, setLoading] = createSignal(true);
|
||||
const [search, setSearch] = createSignal('');
|
||||
const [err, setErr] = createSignal('');
|
||||
const [search, setSearch] = createSignal("");
|
||||
const [err, setErr] = createSignal("");
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setErr('');
|
||||
setErr("");
|
||||
try {
|
||||
const [catRes, artRes] = await Promise.all([
|
||||
apiFetch('/api/kb/categories'),
|
||||
apiFetch("/api/kb/categories"),
|
||||
apiFetch(`/api/kb/articles?role=${encodeURIComponent(props.roleKey)}&page=1&limit=200`),
|
||||
]);
|
||||
const catJson = await catRes.json().catch(() => ({}));
|
||||
|
|
@ -61,12 +68,16 @@ export default function HelpCenterDashboardPage(props: Props) {
|
|||
: Array.isArray(artJson)
|
||||
? artJson
|
||||
: [];
|
||||
setArticles(rawArticles.map(normalizeArticle).filter((a: Article) => Boolean(a.id && a.slug && a.title)));
|
||||
setArticles(
|
||||
rawArticles
|
||||
.map(normalizeArticle)
|
||||
.filter((a: Article) => Boolean(a.id && a.slug && a.title))
|
||||
);
|
||||
}
|
||||
|
||||
if (!catRes.ok && !artRes.ok) setErr('Failed to load help center resources.');
|
||||
if (!catRes.ok && !artRes.ok) setErr("Failed to load help center resources.");
|
||||
} catch {
|
||||
setErr('Network error while loading help center.');
|
||||
setErr("Network error while loading help center.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -77,42 +88,108 @@ export default function HelpCenterDashboardPage(props: Props) {
|
|||
const filtered = createMemo(() => {
|
||||
const q = search().trim().toLowerCase();
|
||||
if (!q) return articles();
|
||||
return articles().filter((a) =>
|
||||
String(a.title || '').toLowerCase().includes(q)
|
||||
|| String(a.summary || '').toLowerCase().includes(q)
|
||||
|| String(a.category || '').toLowerCase().includes(q));
|
||||
return articles().filter(
|
||||
(a) =>
|
||||
String(a.title || "")
|
||||
.toLowerCase()
|
||||
.includes(q) ||
|
||||
String(a.summary || "")
|
||||
.toLowerCase()
|
||||
.includes(q) ||
|
||||
String(a.category || "")
|
||||
.toLowerCase()
|
||||
.includes(q)
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: 'grid', gap: '14px', 'max-width': '980px' }}>
|
||||
<div style={{ display: "grid", gap: "14px", "max-width": "980px" }}>
|
||||
<div style={CARD}>
|
||||
<p style={{ margin: '0', 'font-size': '22px', 'font-weight': '800', color: '#0D0D2A' }}>Help Center</p>
|
||||
<p style={{ margin: '4px 0 0', 'font-size': '13px', color: '#6B7280' }}>Find guides and articles for your role.</p>
|
||||
<p style={{ margin: "0", "font-size": "22px", "font-weight": "800", color: "#0D0D2A" }}>
|
||||
Help Center
|
||||
</p>
|
||||
<p style={{ margin: "4px 0 0", "font-size": "13px", color: "#6B7280" }}>
|
||||
Find guides and articles for your role.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Show when={err()}>
|
||||
<div style={{ ...CARD, border: '1px solid #FECACA', background: '#FEF2F2', padding: '12px 14px', color: '#B91C1C', 'font-size': '13px', 'font-weight': '600' }}>{err()}</div>
|
||||
<div
|
||||
style={{
|
||||
...CARD,
|
||||
border: "1px solid #FECACA",
|
||||
background: "#FEF2F2",
|
||||
padding: "12px 14px",
|
||||
color: "#B91C1C",
|
||||
"font-size": "13px",
|
||||
"font-weight": "600",
|
||||
}}
|
||||
>
|
||||
{err()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div style={{ ...CARD, display: 'grid', gap: '10px' }}>
|
||||
<div style={{ display: 'flex', gap: '10px', 'align-items': 'center' }}>
|
||||
<input value={search()} onInput={(e) => setSearch(e.currentTarget.value)} style={INPUT} placeholder="Search help articles" />
|
||||
<button type="button" onClick={loadData} style={BTN_GHOST}>Refresh</button>
|
||||
<div style={{ ...CARD, display: "grid", gap: "10px" }}>
|
||||
<div style={{ display: "flex", gap: "10px", "align-items": "center" }}>
|
||||
<input
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
style={INPUT}
|
||||
placeholder="Search help articles"
|
||||
/>
|
||||
<button type="button" onClick={loadData} style={BTN_GHOST}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<Show when={loading()}>
|
||||
<p style={{ margin: '0', color: '#9CA3AF', 'font-size': '13px' }}>Loading help center...</p>
|
||||
<p style={{ margin: "0", color: "#9CA3AF", "font-size": "13px" }}>
|
||||
Loading help center...
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={!loading() && categories().length > 0}>
|
||||
<div style={CARD}>
|
||||
<p style={{ margin: '0 0 10px', 'font-size': '16px', 'font-weight': '700', color: '#111827' }}>Categories</p>
|
||||
<div style={{ display: 'grid', 'grid-template-columns': 'repeat(3,minmax(0,1fr))', gap: '10px' }}>
|
||||
<p
|
||||
style={{
|
||||
margin: "0 0 10px",
|
||||
"font-size": "16px",
|
||||
"font-weight": "700",
|
||||
color: "#111827",
|
||||
}}
|
||||
>
|
||||
Categories
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
"grid-template-columns": "repeat(3,minmax(0,1fr))",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<For each={categories().slice(0, 6)}>
|
||||
{(cat) => (
|
||||
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '10px', padding: '10px', background: '#FCFCFD' }}>
|
||||
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '700', color: '#111827' }}>{cat.name}</p>
|
||||
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>{cat.description || 'Knowledge base category'}</p>
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #E5E7EB",
|
||||
"border-radius": "10px",
|
||||
padding: "10px",
|
||||
background: "#FCFCFD",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
margin: "0",
|
||||
"font-size": "13px",
|
||||
"font-weight": "700",
|
||||
color: "#111827",
|
||||
}}
|
||||
>
|
||||
{cat.name}
|
||||
</p>
|
||||
<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>
|
||||
{cat.description || "Knowledge base category"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
|
@ -121,17 +198,58 @@ export default function HelpCenterDashboardPage(props: Props) {
|
|||
</Show>
|
||||
|
||||
<div style={CARD}>
|
||||
<p style={{ margin: '0 0 10px', 'font-size': '16px', 'font-weight': '700', color: '#111827' }}>Articles</p>
|
||||
<p
|
||||
style={{
|
||||
margin: "0 0 10px",
|
||||
"font-size": "16px",
|
||||
"font-weight": "700",
|
||||
color: "#111827",
|
||||
}}
|
||||
>
|
||||
Articles
|
||||
</p>
|
||||
<Show when={!loading() && filtered().length === 0}>
|
||||
<p style={{ margin: '0', color: '#6B7280', 'font-size': '13px' }}>No articles found.</p>
|
||||
<p style={{ margin: "0", color: "#6B7280", "font-size": "13px" }}>No articles found.</p>
|
||||
</Show>
|
||||
<Show when={filtered().length > 0}>
|
||||
<div style={{ display: 'grid', gap: '8px' }}>
|
||||
<div style={{ display: "flex", "flex-direction": "column", gap: "10px" }}>
|
||||
<For each={filtered()}>
|
||||
{(a) => (
|
||||
<a href={`/help-center/article/${a.slug}`} style={{ display: 'block', border: '1px solid #E5E7EB', 'border-radius': '10px', padding: '10px', background: '#FCFCFD', color: 'inherit', 'text-decoration': 'none' }}>
|
||||
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '700', color: '#111827' }}>{a.title}</p>
|
||||
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>{a.summary || 'Open article'}</p>
|
||||
<a
|
||||
href={`/help-center/article/${a.slug}`}
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
border: "1px solid #E5E7EB",
|
||||
"border-radius": "10px",
|
||||
padding: "12px 14px",
|
||||
background: "#FCFCFD",
|
||||
color: "inherit",
|
||||
"text-decoration": "none",
|
||||
"text-align": "left",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
margin: "0",
|
||||
"font-size": "14px",
|
||||
"font-weight": "700",
|
||||
color: "#111827",
|
||||
"line-height": "1.4",
|
||||
}}
|
||||
>
|
||||
{a.title}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
margin: "6px 0 0",
|
||||
"font-size": "13px",
|
||||
color: "#6B7280",
|
||||
"line-height": "1.5",
|
||||
}}
|
||||
>
|
||||
{a.summary || "Open article"}
|
||||
</p>
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,5 @@
|
|||
import { HELP_CENTER_SEED_ARTICLES, HELP_CENTER_SEED_CATEGORIES } from '~/data/help-center-seed';
|
||||
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;
|
||||
|
|
@ -7,10 +8,10 @@ export type HelpArticle = {
|
|||
summary: string;
|
||||
categoryKey: string;
|
||||
category: string;
|
||||
role: 'ALL' | 'company' | 'jobSeeker' | 'professional' | 'customer' | 'platform';
|
||||
role: "ALL" | "company" | "jobSeeker" | "professional" | "customer" | "platform";
|
||||
tags: string[];
|
||||
updatedAt: string;
|
||||
content: string;
|
||||
content: ContentBlock[] | string;
|
||||
};
|
||||
|
||||
export type HelpCategory = {
|
||||
|
|
@ -27,9 +28,9 @@ export async function fetchHelpCenterArticles(input: {
|
|||
q?: string;
|
||||
}): Promise<HelpArticle[]> {
|
||||
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);
|
||||
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()}`);
|
||||
|
|
@ -41,7 +42,7 @@ export async function fetchHelpCenterArticles(input: {
|
|||
// 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');
|
||||
const allRes = await fetch("/api/kb/articles");
|
||||
if (allRes.ok) {
|
||||
const allData = await allRes.json();
|
||||
const allRaw: any[] = Array.isArray(allData) ? allData : (allData.articles ?? []);
|
||||
|
|
@ -61,7 +62,7 @@ export async function fetchHelpCenterArticles(input: {
|
|||
|
||||
export async function fetchHelpCenterCategories(): Promise<HelpCategory[]> {
|
||||
try {
|
||||
const res = await fetch('/api/kb/categories');
|
||||
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 ?? []);
|
||||
|
|
@ -79,7 +80,8 @@ export async function fetchHelpCenterCategories(): Promise<HelpCategory[]> {
|
|||
export async function fetchArticleBySlug(slug: string): Promise<HelpArticle | null> {
|
||||
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;
|
||||
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) {
|
||||
|
|
@ -96,12 +98,22 @@ export async function fetchRelatedArticles(input: {
|
|||
limit?: number;
|
||||
}): Promise<HelpArticle[]> {
|
||||
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 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);
|
||||
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);
|
||||
|
|
@ -111,24 +123,37 @@ export async function fetchRelatedArticles(input: {
|
|||
// ── 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 ?? 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'],
|
||||
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: raw.content ?? raw.body ?? '',
|
||||
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[] {
|
||||
export function listHelpCenterArticles(_input: {
|
||||
role?: string;
|
||||
categoryKey?: string;
|
||||
q?: string;
|
||||
}): HelpArticle[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -141,21 +166,37 @@ export function getArticleBySlug(_slug: string): HelpArticle | 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();
|
||||
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[] {
|
||||
function filterArticles(
|
||||
items: HelpArticle[],
|
||||
input: { role?: string; categoryKey?: string; q?: string }
|
||||
): HelpArticle[] {
|
||||
let filtered = items;
|
||||
|
||||
if (input.role && input.role !== 'ALL') {
|
||||
if (input.role && input.role !== "ALL") {
|
||||
const roleNeedle = input.role.toLowerCase();
|
||||
filtered = filtered.filter((a) => a.role === 'ALL' || String(a.role).toLowerCase() === roleNeedle);
|
||||
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)));
|
||||
filtered = filtered.filter((a) =>
|
||||
[a.categoryKey, a.category].some((v) => (v || "").toLowerCase().includes(needle))
|
||||
);
|
||||
}
|
||||
|
||||
if (input.q) {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
import { A, useParams } from '@solidjs/router';
|
||||
import { Meta, Title } from '@solidjs/meta';
|
||||
import { Show, For, createSignal, createResource, onCleanup, onMount, createMemo } from 'solid-js';
|
||||
import { fetchArticleBySlug, fetchRelatedArticles } from '~/lib/help-center';
|
||||
import PublicBackground from '~/components/PublicBackground';
|
||||
import PublicHeader from '~/components/PublicHeader';
|
||||
import PublicFooter from '~/components/PublicFooter';
|
||||
import { A, useParams } from "@solidjs/router";
|
||||
import { Meta, Title } from "@solidjs/meta";
|
||||
import { Show, For, createSignal, createResource, onCleanup, onMount, createMemo } from "solid-js";
|
||||
import { fetchArticleBySlug, fetchRelatedArticles } from "~/lib/help-center";
|
||||
import PublicBackground from "~/components/PublicBackground";
|
||||
import PublicHeader from "~/components/PublicHeader";
|
||||
import PublicFooter from "~/components/PublicFooter";
|
||||
import ArticleContent from "~/components/ArticleContent";
|
||||
|
||||
function categoryTitle(input: string) {
|
||||
return input
|
||||
.split('-')
|
||||
.split("-")
|
||||
.filter(Boolean)
|
||||
.map((chunk) => chunk[0].toUpperCase() + chunk.slice(1))
|
||||
.join(' ');
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export default function HelpCenterArticlePage() {
|
||||
const params = useParams();
|
||||
const slug = createMemo(() => String(params.slug || '').trim());
|
||||
const slug = createMemo(() => String(params.slug || "").trim());
|
||||
const [scrollY, setScrollY] = createSignal(0);
|
||||
|
||||
const [article] = createResource(() => params.slug, fetchArticleBySlug);
|
||||
|
|
@ -24,21 +25,30 @@ export default function HelpCenterArticlePage() {
|
|||
() => article(),
|
||||
(item) => (item ? fetchRelatedArticles({ article: item, limit: 4 }) : [])
|
||||
);
|
||||
const canonical = createMemo(() => `https://test121.nxtgauge.com/help-center/article/${encodeURIComponent(slug())}`);
|
||||
const canonical = createMemo(
|
||||
() => `https://test121.nxtgauge.com/help-center/article/${encodeURIComponent(slug())}`
|
||||
);
|
||||
const pageTitle = createMemo(() => {
|
||||
const a = article();
|
||||
return a ? `${a.title} | Nxtgauge Help Center` : 'Help Center Article | Nxtgauge';
|
||||
return a ? `${a.title} | Nxtgauge Help Center` : "Help Center Article | Nxtgauge";
|
||||
});
|
||||
const pageDescription = createMemo(() => {
|
||||
const a = article();
|
||||
return a?.summary || 'Read support and product guidance from Nxtgauge Help Center.';
|
||||
return a?.summary || "Read support and product guidance from Nxtgauge Help Center.";
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const onScroll = () => setScrollY(window.scrollY || 0);
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
onCleanup(() => window.removeEventListener('scroll', onScroll));
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
onCleanup(() => window.removeEventListener("scroll", onScroll));
|
||||
});
|
||||
|
||||
const formattedDate = createMemo(() => {
|
||||
const a = article();
|
||||
if (!a?.updatedAt) return "";
|
||||
const date = new Date(a.updatedAt);
|
||||
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -58,82 +68,489 @@ export default function HelpCenterArticlePage() {
|
|||
<div class="lp-content">
|
||||
<PublicHeader />
|
||||
|
||||
{/* Breadcrumb - Dark */}
|
||||
<section style={{ padding: "20px 0 0" }}>
|
||||
<div class="container" style={{ "max-width": "960px" }}>
|
||||
<nav
|
||||
style={{
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
gap: "8px",
|
||||
"font-size": "14px",
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
}}
|
||||
>
|
||||
<A
|
||||
href="/help-center"
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.6)",
|
||||
"text-decoration": "none",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
gap: "6px",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
<polyline points="9,22 9,12 15,12 15,22" />
|
||||
</svg>
|
||||
Help Center
|
||||
</A>
|
||||
<span style={{ color: "rgba(255,255,255,0.3)" }}>/</span>
|
||||
<Show when={article()}>
|
||||
{(a) => (
|
||||
<span style={{ color: "#fd6116", "font-weight": "600" }}>
|
||||
{a().category || categoryTitle(a().categoryKey)}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Loading State */}
|
||||
<Show when={article.loading}>
|
||||
<section class="public-section scene-dark">
|
||||
<div class="container panel panel-light" style={{ 'max-width': '960px' }}>
|
||||
<p style="color:#94a3b8;padding:40px 0;text-align:center">Loading article…</p>
|
||||
<div class="container" style={{ "max-width": "960px" }}>
|
||||
<div
|
||||
class="panel panel-light"
|
||||
style={{ padding: "60px 40px", "text-align": "center" }}
|
||||
>
|
||||
<div
|
||||
class="spinner"
|
||||
style={{
|
||||
width: "40px",
|
||||
height: "40px",
|
||||
border: "3px solid rgba(253,97,22,0.2)",
|
||||
"border-top-color": "#fd6116",
|
||||
"border-radius": "50%",
|
||||
animation: "spin 1s linear infinite",
|
||||
margin: "0 auto 20px",
|
||||
}}
|
||||
/>
|
||||
<p style={{ color: "#6B7280", margin: 0 }}>Loading article...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
|
||||
{/* Not Found State */}
|
||||
<Show when={!article.loading && !article()}>
|
||||
<section class="public-section scene-dark">
|
||||
<div class="container panel panel-light">
|
||||
<h1 class="title">Article not found</h1>
|
||||
<p class="subtitle">The requested Help Center article is unavailable.</p>
|
||||
<div class="actions">
|
||||
<A class="btn primary" href="/help-center">Back to Help Center</A>
|
||||
<div class="container" style={{ "max-width": "960px" }}>
|
||||
<div
|
||||
class="panel panel-light"
|
||||
style={{ padding: "60px 40px", "text-align": "center" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
background: "rgba(253,97,22,0.1)",
|
||||
"border-radius": "16px",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
margin: "0 auto 24px",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#fd6116"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1
|
||||
style={{
|
||||
margin: "0 0 12px",
|
||||
color: "#111827",
|
||||
"font-size": "28px",
|
||||
"font-weight": "700",
|
||||
}}
|
||||
>
|
||||
Article not found
|
||||
</h1>
|
||||
<p style={{ margin: "0 0 24px", color: "#6B7280", "font-size": "16px" }}>
|
||||
The requested Help Center article is unavailable or has been moved.
|
||||
</p>
|
||||
<A class="btn primary" href="/help-center">
|
||||
Back to Help Center
|
||||
</A>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
|
||||
{/* Article Content */}
|
||||
<Show when={!article.loading && article()}>
|
||||
{(a) => (
|
||||
<>
|
||||
<section class="public-section scene-dark">
|
||||
<div class="container panel panel-light" style={{ 'max-width': '960px' }}>
|
||||
<p class="eyebrow">{a().category || categoryTitle(a().categoryKey)}</p>
|
||||
<h1 class="title">{a().title}</h1>
|
||||
<p class="subtitle">{a().summary}</p>
|
||||
|
||||
<Show when={a().tags.length > 0}>
|
||||
<div class="help-article-tags" style={{ 'margin-top': '10px' }}>
|
||||
<For each={a().tags}>{(tag) => <span class="help-article-tag">{tag}</span>}</For>
|
||||
{/* Dark Article Header */}
|
||||
<section class="public-section scene-dark" style={{ padding: "20px 0 30px" }}>
|
||||
<div class="container" style={{ "max-width": "960px" }}>
|
||||
<div style={{ padding: "0 0 20px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
"align-items": "center",
|
||||
gap: "6px",
|
||||
background: "rgba(253,97,22,0.15)",
|
||||
color: "#fd6116",
|
||||
padding: "6px 14px",
|
||||
"border-radius": "20px",
|
||||
"font-size": "12px",
|
||||
"font-weight": "700",
|
||||
"text-transform": "uppercase",
|
||||
"letter-spacing": "0.5px",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
||||
</svg>
|
||||
{a().category || categoryTitle(a().categoryKey)}
|
||||
</div>
|
||||
<h1
|
||||
style={{
|
||||
margin: "16px 0 12px",
|
||||
color: "#fff",
|
||||
"font-size": "36px",
|
||||
"font-weight": "800",
|
||||
"line-height": "1.2",
|
||||
}}
|
||||
>
|
||||
{a().title}
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
"font-size": "18px",
|
||||
"line-height": "1.6",
|
||||
}}
|
||||
>
|
||||
{a().summary}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
gap: "12px",
|
||||
"margin-top": "20px",
|
||||
"flex-wrap": "wrap",
|
||||
}}
|
||||
>
|
||||
<Show when={a().tags?.length > 0}>
|
||||
<div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
|
||||
<For each={a().tags}>
|
||||
{(tag) => (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
"align-items": "center",
|
||||
background: "rgba(255,255,255,0.1)",
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
padding: "4px 10px",
|
||||
"border-radius": "6px",
|
||||
"font-size": "12px",
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<p class="note">Updated {new Date(a().updatedAt).toLocaleDateString()}</p>
|
||||
|
||||
<div class="help-article-body" innerHTML={a().content} />
|
||||
|
||||
<div class="actions">
|
||||
<A class="btn" href="/help-center">Back to Help Center</A>
|
||||
<A class="btn primary" href="/contact">Get Started</A>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
gap: "6px",
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
"font-size": "13px",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12,6 12,12 16,14" />
|
||||
</svg>
|
||||
Updated {formattedDate()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="public-section scene-dark">
|
||||
<div class="container panel panel-light" style={{ 'max-width': '960px' }}>
|
||||
{/* Light Content Section */}
|
||||
<section style={{ background: "#F8FAFC", padding: "40px 0 30px" }}>
|
||||
<div class="container" style={{ "max-width": "960px" }}>
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
"border-radius": "20px",
|
||||
border: "1px solid #E5E7EB",
|
||||
padding: "40px",
|
||||
"box-shadow": "0 1px 3px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={a().content}
|
||||
fallback={<p style={{ color: "#6B7280" }}>No content available.</p>}
|
||||
>
|
||||
<ArticleContent blocks={a().content} />
|
||||
</Show>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "12px",
|
||||
"margin-top": "40px",
|
||||
"padding-top": "32px",
|
||||
"border-top": "1px solid #E5E7EB",
|
||||
}}
|
||||
>
|
||||
<A
|
||||
class="btn"
|
||||
href="/help-center"
|
||||
style={{ display: "inline-flex", "align-items": "center", gap: "6px" }}
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="19" y1="12" x2="5" y2="12" />
|
||||
<polyline points="12,19 5,12 12,5" />
|
||||
</svg>
|
||||
Back to Help Center
|
||||
</A>
|
||||
<A class="btn primary" href="/contact">
|
||||
Get Started
|
||||
</A>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related Articles - Light */}
|
||||
<Show when={(relatedArticles() || []).length > 0}>
|
||||
<div style={{ 'margin-bottom': '20px' }}>
|
||||
<h2 style={{ margin: '0 0 10px', 'font-size': '22px' }}>Related Articles</h2>
|
||||
<div style={{ display: 'grid', gap: '10px' }}>
|
||||
<section style={{ background: "#F8FAFC", padding: "20px 0 40px" }}>
|
||||
<div class="container" style={{ "max-width": "960px" }}>
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
"border-radius": "20px",
|
||||
padding: "32px",
|
||||
border: "1px solid #E5E7EB",
|
||||
"box-shadow": "0 1px 3px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
margin: "0 0 20px",
|
||||
color: "#111827",
|
||||
"font-size": "20px",
|
||||
"font-weight": "700",
|
||||
}}
|
||||
>
|
||||
Related Articles
|
||||
</h2>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
"grid-template-columns": "repeat(auto-fit, minmax(260px, 1fr))",
|
||||
gap: "16px",
|
||||
}}
|
||||
>
|
||||
<For each={relatedArticles() || []}>
|
||||
{(related) => (
|
||||
<A href={`/help-center/article/${related.slug}`} style={{ 'text-decoration': 'none' }}>
|
||||
<article style={{ border: '1px solid rgba(255,255,255,0.16)', 'border-radius': '12px', padding: '12px 14px', background: 'rgba(255,255,255,0.02)' }}>
|
||||
<p style={{ margin: 0, color: '#fd6116', 'font-size': '11px', 'font-weight': '700' }}>{related.category || 'General'}</p>
|
||||
<h3 style={{ margin: '5px 0 4px', color: '#fff', 'font-size': '16px' }}>{related.title}</h3>
|
||||
<p style={{ margin: 0, color: 'rgba(255,255,255,0.75)', 'font-size': '13px' }}>{related.summary}</p>
|
||||
<A
|
||||
href={`/help-center/article/${related.slug}`}
|
||||
style={{ "text-decoration": "none" }}
|
||||
>
|
||||
<article
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "8px",
|
||||
background: "#F9FAFB",
|
||||
border: "1px solid #E5E7EB",
|
||||
"border-radius": "12px",
|
||||
padding: "20px",
|
||||
transition: "all 0.2s",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = "#fff";
|
||||
e.currentTarget.style.borderColor = "#fd6116";
|
||||
e.currentTarget.style.boxShadow =
|
||||
"0 4px 12px rgba(253,97,22,0.1)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = "#F9FAFB";
|
||||
e.currentTarget.style.borderColor = "#E5E7EB";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: "#fd6116",
|
||||
"font-size": "11px",
|
||||
"font-weight": "700",
|
||||
"text-transform": "uppercase",
|
||||
"letter-spacing": "0.5px",
|
||||
}}
|
||||
>
|
||||
{related.category || "General"}
|
||||
</span>
|
||||
<h3
|
||||
style={{
|
||||
margin: 0,
|
||||
color: "#111827",
|
||||
"font-size": "15px",
|
||||
"font-weight": "600",
|
||||
"line-height": "1.4",
|
||||
}}
|
||||
>
|
||||
{related.title}
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
color: "#6B7280",
|
||||
"font-size": "13px",
|
||||
"line-height": "1.5",
|
||||
}}
|
||||
>
|
||||
{related.summary}
|
||||
</p>
|
||||
</article>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
|
||||
<h2>Need more help?</h2>
|
||||
<p class="sub">
|
||||
If this article does not solve your issue, send your question with context to support.
|
||||
{/* Help Section - Dark */}
|
||||
<section style={{ padding: "0 0 40px" }}>
|
||||
<div class="container" style={{ "max-width": "960px" }}>
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(135deg, rgba(253,97,22,0.15) 0%, rgba(253,97,22,0.08) 100%)",
|
||||
"border-radius": "20px",
|
||||
padding: "40px",
|
||||
border: "1px solid rgba(253,97,22,0.25)",
|
||||
"text-align": "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "56px",
|
||||
height: "56px",
|
||||
background: "rgba(253,97,22,0.2)",
|
||||
"border-radius": "14px",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
margin: "0 auto 20px",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#fd6116"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
style={{
|
||||
margin: "0 0 12px",
|
||||
color: "#fff",
|
||||
"font-size": "24px",
|
||||
"font-weight": "700",
|
||||
}}
|
||||
>
|
||||
Need more help?
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
margin: "0 0 24px",
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
"font-size": "16px",
|
||||
}}
|
||||
>
|
||||
If this article does not solve your issue, send your question with context to
|
||||
support.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<a class="btn primary" href="mailto:support@nxtgauge.com?subject=Nxtgauge%20Help%20Center%20Question">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "12px",
|
||||
"justify-content": "center",
|
||||
"flex-wrap": "wrap",
|
||||
}}
|
||||
>
|
||||
<a
|
||||
class="btn primary"
|
||||
href="mailto:support@nxtgauge.com?subject=Nxtgauge%20Help%20Center%20Question"
|
||||
style={{ display: "inline-flex", "align-items": "center", gap: "6px" }}
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
|
||||
<polyline points="22,6 12,13 2,6" />
|
||||
</svg>
|
||||
Email support
|
||||
</a>
|
||||
<A class="btn" href="/help-center">Browse more articles</A>
|
||||
<A class="btn" href="/help-center">
|
||||
Browse more articles
|
||||
</A>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { Meta, Title } from '@solidjs/meta';
|
||||
import { For, createResource, createSignal } from 'solid-js';
|
||||
import PublicBackground from '~/components/PublicBackground';
|
||||
import PublicFooter from '~/components/PublicFooter';
|
||||
import PublicHeader from '~/components/PublicHeader';
|
||||
import { fetchHelpCenterArticles, fetchHelpCenterCategories } from '~/lib/help-center';
|
||||
import { A } from "@solidjs/router";
|
||||
import { Meta, Title } from "@solidjs/meta";
|
||||
import { For, createResource, createSignal, Show } from "solid-js";
|
||||
import PublicBackground from "~/components/PublicBackground";
|
||||
import PublicFooter from "~/components/PublicFooter";
|
||||
import PublicHeader from "~/components/PublicHeader";
|
||||
import { fetchHelpCenterArticles, fetchHelpCenterCategories } from "~/lib/help-center";
|
||||
|
||||
export default function HelpCenterPage() {
|
||||
const title = 'Help Center | Nxtgauge';
|
||||
const description = 'Browse Nxtgauge guides for getting started, roles, requests, approvals, and platform troubleshooting.';
|
||||
const canonical = 'https://test121.nxtgauge.com/help-center';
|
||||
const [query, setQuery] = createSignal('');
|
||||
const [category, setCategory] = createSignal('');
|
||||
const title = "Help Center | Nxtgauge";
|
||||
const description =
|
||||
"Browse Nxtgauge guides for getting started, roles, requests, approvals, and platform troubleshooting.";
|
||||
const canonical = "https://test121.nxtgauge.com/help-center";
|
||||
const [query, setQuery] = createSignal("");
|
||||
const [category, setCategory] = createSignal("");
|
||||
|
||||
const [categories] = createResource(fetchHelpCenterCategories);
|
||||
const [articles] = createResource(
|
||||
() => ({ q: query().trim(), categoryKey: category().trim() }),
|
||||
(input) => fetchHelpCenterArticles({ q: input.q || undefined, categoryKey: input.categoryKey || undefined })
|
||||
(input) =>
|
||||
fetchHelpCenterArticles({
|
||||
q: input.q || undefined,
|
||||
categoryKey: input.categoryKey || undefined,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -36,41 +41,336 @@ export default function HelpCenterPage() {
|
|||
<div class="lp-content">
|
||||
<PublicHeader />
|
||||
|
||||
<section class="public-section scene-dark lp-section">
|
||||
<div class="container panel panel-dark" style={{ padding: '24px' }}>
|
||||
<h1 style={{ margin: '0 0 8px', color: '#fff', 'font-size': '32px', 'font-weight': '800' }}>Help Center</h1>
|
||||
<p style={{ margin: '0 0 20px', color: 'rgba(255,255,255,0.8)' }}>Browse guides and platform FAQs.</p>
|
||||
{/* Dark Hero Section */}
|
||||
<section class="public-section scene-dark" style={{ padding: "60px 0 50px" }}>
|
||||
<div class="container" style={{ "max-width": "900px", "text-align": "center" }}>
|
||||
<h1
|
||||
style={{
|
||||
margin: "0 0 12px",
|
||||
color: "#fff",
|
||||
"font-size": "42px",
|
||||
"font-weight": "800",
|
||||
}}
|
||||
>
|
||||
How can we help you?
|
||||
</h1>
|
||||
<p style={{ margin: "0 0 32px", color: "rgba(255,255,255,0.7)", "font-size": "18px" }}>
|
||||
Browse guides and find answers to common questions
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'grid', gap: '12px', 'grid-template-columns': '2fr 1fr', 'margin-bottom': '20px' }}>
|
||||
{/* Search Bar - Dark */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "12px",
|
||||
"max-width": "700px",
|
||||
margin: "0 auto",
|
||||
"flex-wrap": "wrap",
|
||||
}}
|
||||
>
|
||||
<div style={{ position: "relative", flex: "1", "min-width": "280px" }}>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.4)"
|
||||
stroke-width="2"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "16px",
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
}}
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
<input
|
||||
class="input"
|
||||
placeholder="Search help articles"
|
||||
style={{
|
||||
width: "100%",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
"border-radius": "14px",
|
||||
padding: "16px 18px 16px 48px",
|
||||
color: "#fff",
|
||||
"font-size": "16px",
|
||||
outline: "none",
|
||||
transition: "all 0.2s",
|
||||
}}
|
||||
placeholder="Search help articles..."
|
||||
value={query()}
|
||||
onInput={(e) => setQuery(e.currentTarget.value)}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.12)";
|
||||
e.currentTarget.style.borderColor = "rgba(253,97,22,0.5)";
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
||||
e.currentTarget.style.borderColor = "rgba(255,255,255,0.15)";
|
||||
}}
|
||||
/>
|
||||
<select class="input" value={category()} onChange={(e) => setCategory(e.currentTarget.value)}>
|
||||
<option value="">All categories</option>
|
||||
</div>
|
||||
<select
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
"border-radius": "14px",
|
||||
padding: "16px 20px",
|
||||
color: "#fff",
|
||||
"font-size": "16px",
|
||||
outline: "none",
|
||||
cursor: "pointer",
|
||||
"min-width": "180px",
|
||||
}}
|
||||
value={category()}
|
||||
onChange={(e) => setCategory(e.currentTarget.value)}
|
||||
>
|
||||
<option value="" style={{ background: "#1a1a2e", color: "#fff" }}>
|
||||
All categories
|
||||
</option>
|
||||
<For each={categories() || []}>
|
||||
{(c) => <option value={c.key}>{c.title}</option>}
|
||||
{(c) => (
|
||||
<option value={c.key} style={{ background: "#1a1a2e", color: "#fff" }}>
|
||||
{c.title}
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div style={{ display: 'grid', gap: '10px' }}>
|
||||
{/* Content Section - Transparent background to show parallax */}
|
||||
<section class="public-section scene-dark" style={{ padding: "60px 0 80px" }}>
|
||||
<div class="container" style={{ "max-width": "1100px" }}>
|
||||
{/* Categories - Glass Cards */}
|
||||
<Show when={!query() && !category()}>
|
||||
<div style={{ marginBottom: "60px" }}>
|
||||
<h2
|
||||
style={{
|
||||
margin: "0 0 28px",
|
||||
color: "#fff",
|
||||
"font-size": "24px",
|
||||
"font-weight": "700",
|
||||
}}
|
||||
>
|
||||
Browse by Category
|
||||
</h2>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
"grid-template-columns": "repeat(auto-fill, minmax(260px, 1fr))",
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
<For each={categories() || []}>
|
||||
{(c) => (
|
||||
<button
|
||||
onClick={() => setCategory(c.key)}
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "12px",
|
||||
background: "rgba(255,255,255,0.95)",
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
"border-radius": "16px",
|
||||
padding: "28px",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
"text-align": "left",
|
||||
"box-shadow": "0 4px 20px rgba(0,0,0,0.1)",
|
||||
"backdrop-filter": "blur(10px)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = "#fd6116";
|
||||
e.currentTarget.style.boxShadow = "0 8px 30px rgba(253,97,22,0.2)";
|
||||
e.currentTarget.style.transform = "translateY(-2px)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = "rgba(255,255,255,0.2)";
|
||||
e.currentTarget.style.boxShadow = "0 4px 20px rgba(0,0,0,0.1)";
|
||||
e.currentTarget.style.transform = "translateY(0)";
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
background: "linear-gradient(135deg, #FFF7ED 0%, #FED7AA 100%)",
|
||||
"border-radius": "12px",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#EA580C"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
margin: "4px 0 0",
|
||||
color: "#111827",
|
||||
"font-size": "16px",
|
||||
"font-weight": "600",
|
||||
}}
|
||||
>
|
||||
{c.title}
|
||||
</p>
|
||||
<p style={{ margin: 0, color: "#6B7280", "font-size": "14px" }}>
|
||||
View articles
|
||||
</p>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Articles - Glass Cards */}
|
||||
<div style={{ marginTop: !query() && !category() ? "0" : "20px" }}>
|
||||
<h2
|
||||
style={{
|
||||
margin: "0 0 28px",
|
||||
color: "#fff",
|
||||
"font-size": "24px",
|
||||
"font-weight": "700",
|
||||
}}
|
||||
>
|
||||
{query()
|
||||
? `Search results for "${query()}"`
|
||||
: category()
|
||||
? "Category Articles"
|
||||
: "All Articles"}
|
||||
</h2>
|
||||
<Show when={articles.loading} fallback={null}>
|
||||
<div style={{ display: "flex", "justify-content": "center", padding: "48px" }}>
|
||||
<div
|
||||
style={{
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
border: "3px solid rgba(255,255,255,0.3)",
|
||||
"border-top-color": "#fd6116",
|
||||
"border-radius": "50%",
|
||||
animation: "spin 1s linear infinite",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!articles.loading && (articles() || []).length === 0}>
|
||||
<div
|
||||
style={{
|
||||
"text-align": "center",
|
||||
padding: "48px 20px",
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.5)"
|
||||
stroke-width="1.5"
|
||||
style={{ margin: "0 auto 16px" }}
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
<p style={{ margin: 0, "font-size": "16px" }}>No articles found</p>
|
||||
<p
|
||||
style={{
|
||||
margin: "8px 0 0",
|
||||
"font-size": "14px",
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
}}
|
||||
>
|
||||
Try adjusting your search or filter
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!articles.loading && (articles() || []).length > 0}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
"grid-template-columns": "repeat(auto-fill, minmax(340px, 1fr))",
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
<For each={articles() || []}>
|
||||
{(article) => (
|
||||
<A href={`/help-center/article/${article.slug}`} style={{ 'text-decoration': 'none' }}>
|
||||
<article style={{ border: '1px solid rgba(255,255,255,0.12)', 'border-radius': '12px', padding: '14px 16px', background: 'rgba(255,255,255,0.02)' }}>
|
||||
<p style={{ margin: 0, color: '#fd6116', 'font-size': '12px', 'font-weight': '700' }}>{article.category || 'General'}</p>
|
||||
<h2 style={{ margin: '6px 0 6px', color: '#fff', 'font-size': '18px' }}>{article.title}</h2>
|
||||
<p style={{ margin: 0, color: 'rgba(255,255,255,0.8)', 'font-size': '14px' }}>{article.summary}</p>
|
||||
</article>
|
||||
<A
|
||||
href={`/help-center/article/${article.slug}`}
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "12px",
|
||||
background: "rgba(255,255,255,0.95)",
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
"border-radius": "16px",
|
||||
padding: "28px",
|
||||
"text-decoration": "none",
|
||||
transition: "all 0.2s",
|
||||
"box-shadow": "0 4px 20px rgba(0,0,0,0.1)",
|
||||
"backdrop-filter": "blur(10px)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = "#EA580C";
|
||||
e.currentTarget.style.boxShadow = "0 8px 30px rgba(234,88,12,0.2)";
|
||||
e.currentTarget.style.transform = "translateY(-2px)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = "rgba(255,255,255,0.2)";
|
||||
e.currentTarget.style.boxShadow = "0 4px 20px rgba(0,0,0,0.1)";
|
||||
e.currentTarget.style.transform = "translateY(0)";
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: "#EA580C",
|
||||
"font-size": "12px",
|
||||
"font-weight": "700",
|
||||
"text-transform": "uppercase",
|
||||
"letter-spacing": "0.5px",
|
||||
}}
|
||||
>
|
||||
{article.category || "General"}
|
||||
</span>
|
||||
<h3
|
||||
style={{
|
||||
margin: "4px 0 0",
|
||||
color: "#111827",
|
||||
"font-size": "17px",
|
||||
"font-weight": "600",
|
||||
"line-height": "1.4",
|
||||
}}
|
||||
>
|
||||
{article.title}
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
margin: "4px 0 0",
|
||||
color: "#6B7280",
|
||||
"font-size": "14px",
|
||||
"line-height": "1.6",
|
||||
}}
|
||||
>
|
||||
{article.summary}
|
||||
</p>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
<For each={articles() && (articles() || []).length === 0 ? [1] : []}>
|
||||
{() => <p style={{ margin: 0, color: 'rgba(255,255,255,0.75)' }}>No articles found.</p>}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
11
vite.config.timestamp_1775706024731.js
Normal file
11
vite.config.timestamp_1775706024731.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// vite.config.ts
|
||||
import { defineConfig } from "@solidjs/start/config";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
var vite_config_default = defineConfig({
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
}
|
||||
});
|
||||
export {
|
||||
vite_config_default as default
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue