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:
Ashwin Kumar 2026-04-09 21:52:16 +02:00
parent 01d37fb109
commit 7671ad8e55
7 changed files with 1875 additions and 524 deletions

View 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>
);
}

View file

@ -1,30 +1,37 @@
import { For, Show, createMemo, createSignal, onMount } from 'solid-js'; import { For, Show, createMemo, createSignal, onMount } from "solid-js";
import { BTN_GHOST, CARD, INPUT } from '~/components/DashboardShell'; import { BTN_GHOST, CARD, INPUT } from "~/components/DashboardShell";
import { type RoleKey } from './RoleDashboardShared'; import { type RoleKey } from "./RoleDashboardShared";
const API = '/api/gateway'; const API = "/api/gateway";
type Props = { roleKey: RoleKey }; type Props = { roleKey: RoleKey };
type Category = { id: string; name: string; slug: string; description?: string }; 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) { async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, { return fetch(`${API}${path}`, {
...opts, ...opts,
credentials: 'include', credentials: "include",
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) }, headers: { "Content-Type": "application/json", ...(opts?.headers ?? {}) },
}); });
} }
function normalizeArticle(raw: any): Article { function normalizeArticle(raw: any): Article {
return { return {
id: String(raw?.id || ''), id: String(raw?.id || ""),
title: String(raw?.title || ''), title: String(raw?.title || ""),
slug: String(raw?.slug || ''), slug: String(raw?.slug || ""),
summary: raw?.summary ? String(raw.summary) : '', summary: raw?.summary ? String(raw.summary) : "",
category: String(raw?.category || raw?.category_name || ''), category: String(raw?.category || raw?.category_name || ""),
updatedAt: String(raw?.updatedAt || raw?.updated_at || ''), updatedAt: String(raw?.updatedAt || raw?.updated_at || ""),
}; };
} }
@ -32,15 +39,15 @@ export default function HelpCenterDashboardPage(props: Props) {
const [categories, setCategories] = createSignal<Category[]>([]); const [categories, setCategories] = createSignal<Category[]>([]);
const [articles, setArticles] = createSignal<Article[]>([]); const [articles, setArticles] = createSignal<Article[]>([]);
const [loading, setLoading] = createSignal(true); const [loading, setLoading] = createSignal(true);
const [search, setSearch] = createSignal(''); const [search, setSearch] = createSignal("");
const [err, setErr] = createSignal(''); const [err, setErr] = createSignal("");
const loadData = async () => { const loadData = async () => {
setLoading(true); setLoading(true);
setErr(''); setErr("");
try { try {
const [catRes, artRes] = await Promise.all([ 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`), apiFetch(`/api/kb/articles?role=${encodeURIComponent(props.roleKey)}&page=1&limit=200`),
]); ]);
const catJson = await catRes.json().catch(() => ({})); const catJson = await catRes.json().catch(() => ({}));
@ -61,12 +68,16 @@ export default function HelpCenterDashboardPage(props: Props) {
: Array.isArray(artJson) : Array.isArray(artJson)
? 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 { } catch {
setErr('Network error while loading help center.'); setErr("Network error while loading help center.");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -77,42 +88,108 @@ export default function HelpCenterDashboardPage(props: Props) {
const filtered = createMemo(() => { const filtered = createMemo(() => {
const q = search().trim().toLowerCase(); const q = search().trim().toLowerCase();
if (!q) return articles(); if (!q) return articles();
return articles().filter((a) => return articles().filter(
String(a.title || '').toLowerCase().includes(q) (a) =>
|| String(a.summary || '').toLowerCase().includes(q) String(a.title || "")
|| String(a.category || '').toLowerCase().includes(q)); .toLowerCase()
.includes(q) ||
String(a.summary || "")
.toLowerCase()
.includes(q) ||
String(a.category || "")
.toLowerCase()
.includes(q)
);
}); });
return ( return (
<div style={{ display: 'grid', gap: '14px', 'max-width': '980px' }}> <div style={{ display: "grid", gap: "14px", "max-width": "980px" }}>
<div style={CARD}> <div style={CARD}>
<p style={{ margin: '0', 'font-size': '22px', 'font-weight': '800', color: '#0D0D2A' }}>Help Center</p> <p style={{ margin: "0", "font-size": "22px", "font-weight": "800", color: "#0D0D2A" }}>
<p style={{ margin: '4px 0 0', 'font-size': '13px', color: '#6B7280' }}>Find guides and articles for your role.</p> Help Center
</p>
<p style={{ margin: "4px 0 0", "font-size": "13px", color: "#6B7280" }}>
Find guides and articles for your role.
</p>
</div> </div>
<Show when={err()}> <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> </Show>
<div style={{ ...CARD, display: 'grid', gap: '10px' }}> <div style={{ ...CARD, display: "grid", gap: "10px" }}>
<div style={{ display: 'flex', gap: '10px', 'align-items': 'center' }}> <div style={{ display: "flex", gap: "10px", "align-items": "center" }}>
<input value={search()} onInput={(e) => setSearch(e.currentTarget.value)} style={INPUT} placeholder="Search help articles" /> <input
<button type="button" onClick={loadData} style={BTN_GHOST}>Refresh</button> 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> </div>
<Show when={loading()}> <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> </Show>
</div> </div>
<Show when={!loading() && categories().length > 0}> <Show when={!loading() && categories().length > 0}>
<div style={CARD}> <div style={CARD}>
<p style={{ margin: '0 0 10px', 'font-size': '16px', 'font-weight': '700', color: '#111827' }}>Categories</p> <p
<div style={{ display: 'grid', 'grid-template-columns': 'repeat(3,minmax(0,1fr))', gap: '10px' }}> 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)}> <For each={categories().slice(0, 6)}>
{(cat) => ( {(cat) => (
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '10px', padding: '10px', background: '#FCFCFD' }}> <div
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '700', color: '#111827' }}>{cat.name}</p> style={{
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>{cat.description || 'Knowledge base category'}</p> 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> </div>
)} )}
</For> </For>
@ -121,17 +198,58 @@ export default function HelpCenterDashboardPage(props: Props) {
</Show> </Show>
<div style={CARD}> <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}> <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>
<Show when={filtered().length > 0}> <Show when={filtered().length > 0}>
<div style={{ display: 'grid', gap: '8px' }}> <div style={{ display: "flex", "flex-direction": "column", gap: "10px" }}>
<For each={filtered()}> <For each={filtered()}>
{(a) => ( {(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' }}> <a
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '700', color: '#111827' }}>{a.title}</p> href={`/help-center/article/${a.slug}`}
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>{a.summary || 'Open article'}</p> 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> </a>
)} )}
</For> </For>

File diff suppressed because it is too large Load diff

View file

@ -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 = { export type HelpArticle = {
id: string; id: string;
@ -7,10 +8,10 @@ export type HelpArticle = {
summary: string; summary: string;
categoryKey: string; categoryKey: string;
category: string; category: string;
role: 'ALL' | 'company' | 'jobSeeker' | 'professional' | 'customer' | 'platform'; role: "ALL" | "company" | "jobSeeker" | "professional" | "customer" | "platform";
tags: string[]; tags: string[];
updatedAt: string; updatedAt: string;
content: string; content: ContentBlock[] | string;
}; };
export type HelpCategory = { export type HelpCategory = {
@ -27,9 +28,9 @@ export async function fetchHelpCenterArticles(input: {
q?: string; q?: string;
}): Promise<HelpArticle[]> { }): Promise<HelpArticle[]> {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (input.role && input.role !== 'ALL') params.set('role', input.role); if (input.role && input.role !== "ALL") params.set("role", input.role);
if (input.categoryKey) params.set('category', input.categoryKey); if (input.categoryKey) params.set("category", input.categoryKey);
if (input.q) params.set('q', input.q); if (input.q) params.set("q", input.q);
try { try {
const res = await fetch(`/api/kb/articles?${params.toString()}`); 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 // Fallback: when backend search returns sparse/empty data, apply local filtering
// so users can still find articles by simple keywords. // so users can still find articles by simple keywords.
if (input.q && items.length === 0) { if (input.q && items.length === 0) {
const allRes = await fetch('/api/kb/articles'); const allRes = await fetch("/api/kb/articles");
if (allRes.ok) { if (allRes.ok) {
const allData = await allRes.json(); const allData = await allRes.json();
const allRaw: any[] = Array.isArray(allData) ? allData : (allData.articles ?? []); const allRaw: any[] = Array.isArray(allData) ? allData : (allData.articles ?? []);
@ -61,7 +62,7 @@ export async function fetchHelpCenterArticles(input: {
export async function fetchHelpCenterCategories(): Promise<HelpCategory[]> { export async function fetchHelpCenterCategories(): Promise<HelpCategory[]> {
try { try {
const res = await fetch('/api/kb/categories'); const res = await fetch("/api/kb/categories");
if (!res.ok) return HELP_CENTER_SEED_CATEGORIES; if (!res.ok) return HELP_CENTER_SEED_CATEGORIES;
const data = await res.json(); const data = await res.json();
const raw: any[] = Array.isArray(data) ? data : (data.categories ?? []); 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> { export async function fetchArticleBySlug(slug: string): Promise<HelpArticle | null> {
try { try {
const res = await fetch(`/api/kb/articles/${slug}`); 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 data = await res.json();
const normalized = normalizeArticle(data); const normalized = normalizeArticle(data);
if (!normalized.slug) { if (!normalized.slug) {
@ -96,12 +98,22 @@ export async function fetchRelatedArticles(input: {
limit?: number; limit?: number;
}): Promise<HelpArticle[]> { }): Promise<HelpArticle[]> {
try { try {
const res = await fetch('/api/kb/articles'); const res = await fetch("/api/kb/articles");
if (!res.ok) return pickRelated(HELP_CENTER_SEED_ARTICLES as HelpArticle[], input.article, input.limit ?? 4); if (!res.ok)
return pickRelated(
HELP_CENTER_SEED_ARTICLES as HelpArticle[],
input.article,
input.limit ?? 4
);
const data = await res.json(); const data = await res.json();
const raw: any[] = Array.isArray(data) ? data : (data.articles ?? []); const raw: any[] = Array.isArray(data) ? data : (data.articles ?? []);
const all = raw.map(normalizeArticle); 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); return pickRelated(all, input.article, input.limit ?? 4);
} catch { } catch {
return pickRelated(HELP_CENTER_SEED_ARTICLES as HelpArticle[], input.article, input.limit ?? 4); return pickRelated(HELP_CENTER_SEED_ARTICLES as HelpArticle[], input.article, input.limit ?? 4);
@ -111,24 +123,37 @@ export async function fetchRelatedArticles(input: {
// ── Normalizer ──────────────────────────────────────────────────────────────── // ── Normalizer ────────────────────────────────────────────────────────────────
function normalizeArticle(raw: any): HelpArticle { 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 { return {
id: raw.id ?? '', id: raw.id ?? "",
slug: raw.slug ?? '', slug: raw.slug ?? "",
title: raw.title ?? '', title: raw.title ?? "",
summary: raw.summary ?? raw.content?.slice(0, 160) ?? '', summary: raw.summary ?? (typeof content === "string" ? content.slice(0, 160) : "") ?? "",
categoryKey: raw.categoryKey ?? raw.category_key ?? raw.categorySlug ?? '', categoryKey: raw.categoryKey ?? raw.category_key ?? raw.categorySlug ?? "",
category: raw.category ?? raw.categoryKey ?? '', category: raw.category ?? raw.categoryKey ?? "",
role: (raw.role ?? 'ALL') as HelpArticle['role'], role: (raw.role ?? "ALL") as HelpArticle["role"],
tags: Array.isArray(raw.tags) ? raw.tags : [], tags: Array.isArray(raw.tags) ? raw.tags : [],
updatedAt: raw.updatedAt ?? raw.updated_at ?? new Date().toISOString(), 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) ───────────────────── // ── Legacy sync shims (kept for any remaining call sites) ─────────────────────
// These return empty data synchronously — pages should use the async fetch* functions above. // 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 []; return [];
} }
@ -141,21 +166,37 @@ export function getArticleBySlug(_slug: string): HelpArticle | null {
} }
function articleMatchesQuery(article: HelpArticle, needle: string): boolean { 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); 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; let filtered = items;
if (input.role && input.role !== 'ALL') { if (input.role && input.role !== "ALL") {
const roleNeedle = input.role.toLowerCase(); 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) { if (input.categoryKey) {
const needle = input.categoryKey.toLowerCase(); 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) { if (input.q) {

View file

@ -1,22 +1,23 @@
import { A, useParams } from '@solidjs/router'; import { A, useParams } from "@solidjs/router";
import { Meta, Title } from '@solidjs/meta'; import { Meta, Title } from "@solidjs/meta";
import { Show, For, createSignal, createResource, onCleanup, onMount, createMemo } from 'solid-js'; import { Show, For, createSignal, createResource, onCleanup, onMount, createMemo } from "solid-js";
import { fetchArticleBySlug, fetchRelatedArticles } from '~/lib/help-center'; import { fetchArticleBySlug, fetchRelatedArticles } from "~/lib/help-center";
import PublicBackground from '~/components/PublicBackground'; import PublicBackground from "~/components/PublicBackground";
import PublicHeader from '~/components/PublicHeader'; import PublicHeader from "~/components/PublicHeader";
import PublicFooter from '~/components/PublicFooter'; import PublicFooter from "~/components/PublicFooter";
import ArticleContent from "~/components/ArticleContent";
function categoryTitle(input: string) { function categoryTitle(input: string) {
return input return input
.split('-') .split("-")
.filter(Boolean) .filter(Boolean)
.map((chunk) => chunk[0].toUpperCase() + chunk.slice(1)) .map((chunk) => chunk[0].toUpperCase() + chunk.slice(1))
.join(' '); .join(" ");
} }
export default function HelpCenterArticlePage() { export default function HelpCenterArticlePage() {
const params = useParams(); const params = useParams();
const slug = createMemo(() => String(params.slug || '').trim()); const slug = createMemo(() => String(params.slug || "").trim());
const [scrollY, setScrollY] = createSignal(0); const [scrollY, setScrollY] = createSignal(0);
const [article] = createResource(() => params.slug, fetchArticleBySlug); const [article] = createResource(() => params.slug, fetchArticleBySlug);
@ -24,21 +25,30 @@ export default function HelpCenterArticlePage() {
() => article(), () => article(),
(item) => (item ? fetchRelatedArticles({ article: item, limit: 4 }) : []) (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 pageTitle = createMemo(() => {
const a = article(); 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 pageDescription = createMemo(() => {
const a = article(); 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(() => { onMount(() => {
const onScroll = () => setScrollY(window.scrollY || 0); const onScroll = () => setScrollY(window.scrollY || 0);
onScroll(); onScroll();
window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener("scroll", onScroll, { passive: true });
onCleanup(() => window.removeEventListener('scroll', onScroll)); 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 ( return (
@ -58,82 +68,489 @@ export default function HelpCenterArticlePage() {
<div class="lp-content"> <div class="lp-content">
<PublicHeader /> <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}> <Show when={article.loading}>
<section class="public-section scene-dark"> <section class="public-section scene-dark">
<div class="container panel panel-light" style={{ 'max-width': '960px' }}> <div class="container" style={{ "max-width": "960px" }}>
<p style="color:#94a3b8;padding:40px 0;text-align:center">Loading article</p> <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> </div>
</section> </section>
</Show> </Show>
{/* Not Found State */}
<Show when={!article.loading && !article()}> <Show when={!article.loading && !article()}>
<section class="public-section scene-dark"> <section class="public-section scene-dark">
<div class="container panel panel-light"> <div class="container" style={{ "max-width": "960px" }}>
<h1 class="title">Article not found</h1> <div
<p class="subtitle">The requested Help Center article is unavailable.</p> class="panel panel-light"
<div class="actions"> style={{ padding: "60px 40px", "text-align": "center" }}
<A class="btn primary" href="/help-center">Back to Help Center</A> >
<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>
</div> </div>
</section> </section>
</Show> </Show>
{/* Article Content */}
<Show when={!article.loading && article()}> <Show when={!article.loading && article()}>
{(a) => ( {(a) => (
<> <>
<section class="public-section scene-dark"> {/* Dark Article Header */}
<div class="container panel panel-light" style={{ 'max-width': '960px' }}> <section class="public-section scene-dark" style={{ padding: "20px 0 30px" }}>
<p class="eyebrow">{a().category || categoryTitle(a().categoryKey)}</p> <div class="container" style={{ "max-width": "960px" }}>
<h1 class="title">{a().title}</h1> <div style={{ padding: "0 0 20px" }}>
<p class="subtitle">{a().summary}</p> <div
style={{
<Show when={a().tags.length > 0}> display: "inline-flex",
<div class="help-article-tags" style={{ 'margin-top': '10px' }}> "align-items": "center",
<For each={a().tags}>{(tag) => <span class="help-article-tag">{tag}</span>}</For> 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> </div>
</Show> </Show>
<div
<p class="note">Updated {new Date(a().updatedAt).toLocaleDateString()}</p> style={{
display: "flex",
<div class="help-article-body" innerHTML={a().content} /> "align-items": "center",
gap: "6px",
<div class="actions"> color: "rgba(255,255,255,0.5)",
<A class="btn" href="/help-center">Back to Help Center</A> "font-size": "13px",
<A class="btn primary" href="/contact">Get Started</A> }}
>
<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>
</div> </div>
</section> </section>
<section class="public-section scene-dark"> {/* Light Content Section */}
<div class="container panel panel-light" style={{ 'max-width': '960px' }}> <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}> <Show when={(relatedArticles() || []).length > 0}>
<div style={{ 'margin-bottom': '20px' }}> <section style={{ background: "#F8FAFC", padding: "20px 0 40px" }}>
<h2 style={{ margin: '0 0 10px', 'font-size': '22px' }}>Related Articles</h2> <div class="container" style={{ "max-width": "960px" }}>
<div style={{ display: 'grid', gap: '10px' }}> <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() || []}> <For each={relatedArticles() || []}>
{(related) => ( {(related) => (
<A href={`/help-center/article/${related.slug}`} style={{ 'text-decoration': 'none' }}> <A
<article style={{ border: '1px solid rgba(255,255,255,0.16)', 'border-radius': '12px', padding: '12px 14px', background: 'rgba(255,255,255,0.02)' }}> href={`/help-center/article/${related.slug}`}
<p style={{ margin: 0, color: '#fd6116', 'font-size': '11px', 'font-weight': '700' }}>{related.category || 'General'}</p> style={{ "text-decoration": "none" }}
<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> <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> </article>
</A> </A>
)} )}
</For> </For>
</div> </div>
</div> </div>
</div>
</section>
</Show> </Show>
<h2>Need more help?</h2> {/* Help Section - Dark */}
<p class="sub"> <section style={{ padding: "0 0 40px" }}>
If this article does not solve your issue, send your question with context to support. <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> </p>
<div class="actions"> <div
<a class="btn primary" href="mailto:support@nxtgauge.com?subject=Nxtgauge%20Help%20Center%20Question"> 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 Email support
</a> </a>
<A class="btn" href="/help-center">Browse more articles</A> <A class="btn" href="/help-center">
Browse more articles
</A>
</div>
</div> </div>
</div> </div>
</section> </section>

View file

@ -1,22 +1,27 @@
import { A } from '@solidjs/router'; import { A } from "@solidjs/router";
import { Meta, Title } from '@solidjs/meta'; import { Meta, Title } from "@solidjs/meta";
import { For, createResource, createSignal } from 'solid-js'; import { For, createResource, createSignal, Show } from "solid-js";
import PublicBackground from '~/components/PublicBackground'; import PublicBackground from "~/components/PublicBackground";
import PublicFooter from '~/components/PublicFooter'; import PublicFooter from "~/components/PublicFooter";
import PublicHeader from '~/components/PublicHeader'; import PublicHeader from "~/components/PublicHeader";
import { fetchHelpCenterArticles, fetchHelpCenterCategories } from '~/lib/help-center'; import { fetchHelpCenterArticles, fetchHelpCenterCategories } from "~/lib/help-center";
export default function HelpCenterPage() { export default function HelpCenterPage() {
const title = 'Help Center | Nxtgauge'; const title = "Help Center | Nxtgauge";
const description = 'Browse Nxtgauge guides for getting started, roles, requests, approvals, and platform troubleshooting.'; const description =
const canonical = 'https://test121.nxtgauge.com/help-center'; "Browse Nxtgauge guides for getting started, roles, requests, approvals, and platform troubleshooting.";
const [query, setQuery] = createSignal(''); const canonical = "https://test121.nxtgauge.com/help-center";
const [category, setCategory] = createSignal(''); const [query, setQuery] = createSignal("");
const [category, setCategory] = createSignal("");
const [categories] = createResource(fetchHelpCenterCategories); const [categories] = createResource(fetchHelpCenterCategories);
const [articles] = createResource( const [articles] = createResource(
() => ({ q: query().trim(), categoryKey: category().trim() }), () => ({ 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 ( return (
@ -36,41 +41,336 @@ export default function HelpCenterPage() {
<div class="lp-content"> <div class="lp-content">
<PublicHeader /> <PublicHeader />
<section class="public-section scene-dark lp-section"> {/* Dark Hero Section */}
<div class="container panel panel-dark" style={{ padding: '24px' }}> <section class="public-section scene-dark" style={{ padding: "60px 0 50px" }}>
<h1 style={{ margin: '0 0 8px', color: '#fff', 'font-size': '32px', 'font-weight': '800' }}>Help Center</h1> <div class="container" style={{ "max-width": "900px", "text-align": "center" }}>
<p style={{ margin: '0 0 20px', color: 'rgba(255,255,255,0.8)' }}>Browse guides and platform FAQs.</p> <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 <input
class="input" style={{
placeholder="Search help articles" 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()} value={query()}
onInput={(e) => setQuery(e.currentTarget.value)} 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)}> </div>
<option value="">All categories</option> <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() || []}> <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> </For>
</select> </select>
</div> </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() || []}> <For each={articles() || []}>
{(article) => ( {(article) => (
<A href={`/help-center/article/${article.slug}`} style={{ 'text-decoration': 'none' }}> <A
<article style={{ border: '1px solid rgba(255,255,255,0.12)', 'border-radius': '12px', padding: '14px 16px', background: 'rgba(255,255,255,0.02)' }}> href={`/help-center/article/${article.slug}`}
<p style={{ margin: 0, color: '#fd6116', 'font-size': '12px', 'font-weight': '700' }}>{article.category || 'General'}</p> style={{
<h2 style={{ margin: '6px 0 6px', color: '#fff', 'font-size': '18px' }}>{article.title}</h2> display: "flex",
<p style={{ margin: 0, color: 'rgba(255,255,255,0.8)', 'font-size': '14px' }}>{article.summary}</p> "flex-direction": "column",
</article> 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> </A>
)} )}
</For> </For>
<For each={articles() && (articles() || []).length === 0 ? [1] : []}> </div>
{() => <p style={{ margin: 0, color: 'rgba(255,255,255,0.75)' }}>No articles found.</p>} </Show>
</For>
</div> </div>
</div> </div>
</section> </section>

View 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
};