Load rule templates from API
This commit is contained in:
@@ -11,13 +11,8 @@ import {
|
||||
} from "../../../../lib/create/templateReviewMapping";
|
||||
import {
|
||||
getGovernanceTemplateCatalogEntry,
|
||||
governanceTemplateIconPath,
|
||||
} from "../../../../lib/templates/governanceTemplateCatalog";
|
||||
|
||||
const FALLBACK_PRESENTATION = {
|
||||
iconPath: governanceTemplateIconPath("consensus"),
|
||||
backgroundColor: "bg-[var(--color-surface-invert-brand-teal)]",
|
||||
};
|
||||
import { TEMPLATE_GRID_FALLBACK_PRESENTATION } from "../../../../lib/templates/templateGridPresentation";
|
||||
|
||||
export interface TemplateReviewCardProps {
|
||||
template: RuleTemplateDto;
|
||||
@@ -37,7 +32,7 @@ export function TemplateReviewCard({
|
||||
size = "L",
|
||||
}: TemplateReviewCardProps) {
|
||||
const catalog = getGovernanceTemplateCatalogEntry(template.slug);
|
||||
const pres = catalog ?? FALLBACK_PRESENTATION;
|
||||
const pres = catalog ?? TEMPLATE_GRID_FALLBACK_PRESENTATION;
|
||||
const categories = templateBodyToCategories(template.body);
|
||||
const summary = templateSummaryFromBody(template.description, template.body);
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Placeholder grid matching GovernanceTemplateGrid layout (loading state).
|
||||
*/
|
||||
export function GovernanceTemplateGridSkeleton({ count }: { count: number }) {
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
flex flex-col gap-[18px]
|
||||
min-[768px]:grid min-[768px]:grid-cols-2 min-[768px]:gap-[18px]
|
||||
min-[1024px]:gap-[24px]
|
||||
"
|
||||
aria-busy
|
||||
aria-label="Loading templates"
|
||||
>
|
||||
{Array.from({ length: count }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="
|
||||
flex min-h-[120px] animate-pulse flex-col gap-3 rounded-[var(--measures-radius-200,8px)]
|
||||
bg-[var(--color-surface-default-secondary,#262626)] p-4
|
||||
min-[640px]:min-h-[140px] min-[640px]:rounded-[var(--measures-radius-300,12px)]
|
||||
min-[1024px]:min-h-[160px] min-[1024px]:rounded-[var(--radius-measures-radius-small)]
|
||||
"
|
||||
>
|
||||
<div className="h-10 w-10 rounded bg-[var(--color-surface-default-tertiary,#404040)] min-[640px]:h-14 min-[640px]:w-14" />
|
||||
<div className="h-4 w-[55%] max-w-[280px] rounded bg-[var(--color-surface-default-tertiary,#404040)]" />
|
||||
<div className="h-3 w-full max-w-[400px] rounded bg-[var(--color-surface-default-tertiary,#404040)]" />
|
||||
<div className="h-3 w-[72%] max-w-[360px] rounded bg-[var(--color-surface-default-tertiary,#404040)]" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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="
|
||||
|
||||
Reference in New Issue
Block a user