Load rule templates from API

This commit is contained in:
adilallo
2026-04-12 21:56:34 -06:00
parent cae4df261e
commit a39b4aa04b
17 changed files with 698 additions and 429 deletions
@@ -1,8 +1,15 @@
"use client";
import { memo } from "react";
import { memo, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { logger } from "../../../../lib/logger";
import {
fetchTemplates,
isTemplatesFetchAborted,
} from "../../../../lib/create/fetchTemplates";
import { GOVERNANCE_TEMPLATE_HOME_SLUGS } from "../../../../lib/templates/governanceTemplateCatalog";
import { gridEntriesForSlugOrderWithCatalogFallback } from "../../../../lib/templates/templateGridPresentation";
import type { TemplateGridCardEntry } from "../../../../lib/templates/templateGridPresentation";
import { RuleStackView } from "./RuleStack.view";
import type { RuleStackProps } from "./RuleStack.types";
@@ -19,8 +26,53 @@ declare global {
}
}
const RuleStackContainer = memo<RuleStackProps>(({ className = "" }) => {
const RuleStackContainer = memo<RuleStackProps>(
({ className = "", initialGridEntries }) => {
const router = useRouter();
const [gridEntries, setGridEntries] = useState<TemplateGridCardEntry[] | null>(
() => initialGridEntries ?? null,
);
useEffect(() => {
if (initialGridEntries !== undefined) {
return;
}
const ac = new AbortController();
let cancelled = false;
void (async () => {
try {
const result = await fetchTemplates({ signal: ac.signal });
if (cancelled) return;
if ("error" in result) {
setGridEntries(
gridEntriesForSlugOrderWithCatalogFallback(
[],
GOVERNANCE_TEMPLATE_HOME_SLUGS,
),
);
return;
}
setGridEntries(
gridEntriesForSlugOrderWithCatalogFallback(
result,
GOVERNANCE_TEMPLATE_HOME_SLUGS,
),
);
} catch (e) {
if (cancelled || isTemplatesFetchAborted(e)) return;
setGridEntries(
gridEntriesForSlugOrderWithCatalogFallback(
[],
GOVERNANCE_TEMPLATE_HOME_SLUGS,
),
);
}
})();
return () => {
cancelled = true;
ac.abort();
};
}, [initialGridEntries]);
const handleTemplateClick = (slug: string) => {
// Basic analytics tracking
@@ -44,9 +96,11 @@ const RuleStackContainer = memo<RuleStackProps>(({ className = "" }) => {
<RuleStackView
className={className}
onTemplateClick={handleTemplateClick}
gridEntries={gridEntries}
/>
);
});
},
);
RuleStackContainer.displayName = "RuleStack";
@@ -1,8 +1,17 @@
import type { TemplateGridCardEntry } from "../../../../lib/templates/templateGridPresentation";
export interface RuleStackProps {
className?: string;
/**
* When set (e.g. from a Server Component), first paint uses this data and
* the client skips the `/api/templates` request.
*/
initialGridEntries?: TemplateGridCardEntry[];
}
export interface RuleStackViewProps {
className: string;
onTemplateClick: (_slug: string) => void;
/** `null` while loading curated templates from the API. */
gridEntries: TemplateGridCardEntry[] | null;
}
@@ -4,14 +4,13 @@ import { useTranslation } from "../../../contexts/MessagesContext";
import SectionHeader from "../SectionHeader";
import Button from "../../buttons/Button";
import { GovernanceTemplateGrid } from "../GovernanceTemplateGrid";
import { getGovernanceTemplatesForHome } from "../../../../lib/templates/governanceTemplateCatalog";
import { GovernanceTemplateGridSkeleton } from "../GovernanceTemplateGrid/GovernanceTemplateGridSkeleton";
import type { RuleStackViewProps } from "./RuleStack.types";
const homeFeaturedTemplates = getGovernanceTemplatesForHome();
export function RuleStackView({
className,
onTemplateClick,
gridEntries,
}: RuleStackViewProps) {
const t = useTranslation("pages.home.ruleStack");
const buttonText = t("button.seeAllTemplates");
@@ -37,10 +36,14 @@ export function RuleStackView({
variant="multi-line"
/>
<GovernanceTemplateGrid
entries={homeFeaturedTemplates}
onTemplateClick={onTemplateClick}
/>
{gridEntries === null ? (
<GovernanceTemplateGridSkeleton count={4} />
) : (
<GovernanceTemplateGrid
entries={gridEntries}
onTemplateClick={onTemplateClick}
/>
)}
<div
className="