Fazekit

Code

"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { helpContent, type HelpArticle, type HelpCategory, type HelpContent } from "./data";
import { Navbar, StatusBanner } from "./sections/header";
import { SearchHero } from "./sections/hero";
import { Categories, Faq, Guides, Popular, Stats } from "./sections/home";
import { Contact, type ContactPayload } from "./sections/contact";
import { ArticleView, CategoryView } from "./sections/article";
import { ChatWidget } from "./sections/chat";
import { Footer } from "./sections/footer";
import { scrollToId } from "./sections/ui";

export type { HelpContent, HelpArticle, HelpCategory, ContactPayload };

export interface HelpCenterTemplateProps {
  /** Override brand, status, categories, articles, FAQ, bot copy or footer. Missing keys fall back to the demo content. */
  content?: Partial<HelpContent>;
  /** Called with the form values when the email form is submitted successfully. */
  onContactSubmit?: (payload: ContactPayload) => void;
  className?: string;
}

type View = { kind: "home" } | { kind: "category"; id: string } | { kind: "article"; id: string };

/**
 * Lumen Help — a support center template.
 * Status banner, sticky navbar, search hero with instant results, topic cards, popular articles,
 * video guides, stats, contact options with an email form, FAQ and footer — plus category and
 * article views (breadcrumbs, helpfulness vote) and a floating chat widget with canned bot replies.
 */
export function HelpCenterTemplate({ content, onContactSubmit, className }: HelpCenterTemplateProps) {
  const c: HelpContent = { ...helpContent, ...content };
  const [view, setView] = React.useState<View>({ kind: "home" });
  const [chatOpen, setChatOpen] = React.useState(false);
  const pending = React.useRef<string | null | undefined>(undefined);

  const catMap = React.useMemo(() => new Map(c.categories.map((x) => [x.id, x])), [c.categories]);
  const artMap = React.useMemo(() => new Map(c.articles.map((x) => [x.id, x])), [c.articles]);
  const inCat = React.useCallback((id: string) => c.articles.filter((a) => a.category === id), [c.articles]);

  React.useLayoutEffect(() => {
    const target = pending.current;
    if (target === undefined) return;
    pending.current = undefined;
    if (target) requestAnimationFrame(() => scrollToId(target));
    else {
      window.scrollTo({ top: 0, behavior: "auto" });
      document.getElementById(view.kind === "article" ? "lh-article-title" : view.kind === "category" ? "lh-category-title" : "lh-hero-title")?.focus({ preventScroll: true });
    }
  }, [view]);

  const go = (v: View, anchor?: string) => {
    pending.current = anchor ?? null;
    setView(v);
  };
  const goHome = (anchor?: string) => {
    if (view.kind === "home") {
      if (anchor) scrollToId(anchor);
      else window.scrollTo({ top: 0, behavior: "smooth" });
      return;
    }
    go({ kind: "home" }, anchor);
  };
  const openArticle = (id: string) => go({ kind: "article", id });
  const openCategory = (id: string) => go({ kind: "category", id });
  const contact = () => goHome("contact");

  const article = view.kind === "article" ? artMap.get(view.id) : undefined;
  const category = view.kind === "category" ? catMap.get(view.id) : article ? catMap.get(article.category) : undefined;

  const footerNav = (label: string) => {
    const map: Record<string, string> = { Topics: "topics", "Popular articles": "popular", "Video guides": "guides", "Contact support": "contact" };
    if (map[label]) goHome(map[label]);
  };

  return (
    <div className={cn("relative w-full bg-background text-foreground antialiased", className)}>
      <a
        href="#lh-main"
        className="sr-only z-[90] rounded-md bg-foreground px-3 py-2 text-sm text-background focus:not-sr-only focus:fixed focus:left-3 focus:top-3"
      >
        Skip to content
      </a>
      <StatusBanner status={c.status} />
      <Navbar brand={c.brand} links={c.nav} onNav={(h) => goHome(h.slice(1))} onHome={() => goHome()} onContact={contact} />

      <main id="lh-main">
        {article ? (
          <ArticleView
            key={article.id}
            article={article}
            category={category}
            siblings={inCat(article.category)}
            related={c.articles.filter((a) => a.id !== article.id && (a.category === article.category || a.tags.some((t) => article.tags.includes(t)))).slice(0, 4)}
            onHome={() => goHome()}
            onCategory={openCategory}
            onOpen={openArticle}
            onContact={contact}
            onChat={() => setChatOpen(true)}
          />
        ) : category ? (
          <CategoryView key={category.id} category={category} articles={inCat(category.id)} onHome={() => goHome("topics")} onOpen={openArticle} />
        ) : (
          <>
            <SearchHero hero={c.hero} articles={c.articles} categoryOf={(id) => catMap.get(id)} onOpen={openArticle} onContact={contact} />
            <Categories categories={c.categories} countOf={(id) => inCat(id).length} topOf={(id) => inCat(id).slice(0, 2)} onCategory={openCategory} onOpen={openArticle} />
            <Popular articles={c.popularIds.map((id) => artMap.get(id)).filter((a): a is HelpArticle => !!a)} categoryOf={(id) => catMap.get(id)} onOpen={openArticle} />
            <Guides guides={c.guides} />
            <Stats stats={c.stats} />
            <Faq items={c.faq} />
            <Contact content={c.contact} onChat={() => setChatOpen(true)} onSubmit={onContactSubmit} />
          </>
        )}
      </main>

      <Footer brand={c.brand} content={c.footer} onNav={footerNav} />
      <ChatWidget open={chatOpen} onOpenChange={setChatOpen} bot={c.bot} articles={c.articles} onOpenArticle={openArticle} />
    </div>
  );
}

export default HelpCenterTemplate;

More in Blog & Docs

View all →