RuleTemplate seed and create flow

This commit is contained in:
adilallo
2026-04-10 22:17:52 -06:00
parent cee81eda16
commit ec5afd1464
47 changed files with 1706 additions and 265 deletions
+45
View File
@@ -0,0 +1,45 @@
/**
* Client fetch for curated rule templates (GET /api/templates).
*/
export type RuleTemplateDto = {
id: string;
slug: string;
title: string;
category: string | null;
description: string | null;
body: unknown;
featured: boolean;
};
type TemplatesResponse = { templates?: RuleTemplateDto[] };
export async function fetchTemplates(): Promise<
RuleTemplateDto[] | { error: string }
> {
try {
const res = await fetch("/api/templates", { credentials: "include" });
const data = (await res.json()) as TemplatesResponse & { error?: string };
if (!res.ok) {
return {
error:
typeof data.error === "string"
? data.error
: "Could not load templates",
};
}
return Array.isArray(data.templates) ? data.templates : [];
} catch {
return { error: "Could not load templates" };
}
}
export async function fetchTemplateBySlug(
slug: string,
): Promise<RuleTemplateDto | null | { error: string }> {
const result = await fetchTemplates();
if ("error" in result) {
return result;
}
return result.find((t) => t.slug === slug) ?? null;
}
+68
View File
@@ -0,0 +1,68 @@
import type { Category } from "../../app/components/cards/RuleCard/RuleCard.types";
import type { ChipOption } from "../../app/components/controls/MultiSelect/MultiSelect.types";
function isDocumentEntry(x: unknown): x is { title: string; body: string } {
if (!x || typeof x !== "object") return false;
const o = x as Record<string, unknown>;
return typeof o.title === "string" && typeof o.body === "string";
}
function isDocumentSection(
x: unknown,
): x is {
categoryName: string;
entries: { title: string; body: string }[];
} {
if (!x || typeof x !== "object") return false;
const o = x as Record<string, unknown>;
if (typeof o.categoryName !== "string") return false;
if (!Array.isArray(o.entries)) return false;
return o.entries.every(isDocumentEntry);
}
/**
* Maps API template `body` (published-rule document shape) to RuleCard category rows.
*/
export function templateBodyToCategories(body: unknown): Category[] {
if (!body || typeof body !== "object") return [];
const sections = (body as Record<string, unknown>).sections;
if (!Array.isArray(sections)) return [];
const out: Category[] = [];
for (const raw of sections) {
if (!isDocumentSection(raw)) continue;
const chipOptions: ChipOption[] = raw.entries.map((e, i) => ({
id: `${raw.categoryName}-${i}`,
label: e.title,
state: "unselected",
}));
out.push({
name: raw.categoryName,
chipOptions,
});
}
return out;
}
/**
* Summary line under tag rows: prefer API description; else first entry bodies (short).
*/
export function templateSummaryFromBody(
description: string | null | undefined,
body: unknown,
): string {
const d = typeof description === "string" ? description.trim() : "";
if (d.length > 0) return d;
if (!body || typeof body !== "object") return "";
const sections = (body as Record<string, unknown>).sections;
if (!Array.isArray(sections)) return "";
for (const s of sections) {
if (!isDocumentSection(s)) continue;
const first = s.entries[0];
if (isDocumentEntry(first) && first.body.trim()) {
return first.body.trim();
}
}
return "";
}