Template recommendation implemented

This commit is contained in:
adilallo
2026-04-29 19:24:50 -06:00
parent c4c74ecdb4
commit a4f0c4bf27
20 changed files with 899 additions and 82 deletions
@@ -6,8 +6,13 @@ import HeaderLockup from "../../components/type/HeaderLockup";
import { GovernanceTemplateGrid } from "../../components/sections/GovernanceTemplateGrid";
import type { TemplateGridCardEntry } from "../../../lib/templates/templateGridPresentation";
import { prepareFreshCreateFlowEntry } from "../../(app)/create/utils/prepareFreshCreateFlowEntry";
import { buildTemplateReviewHref } from "../../(app)/create/utils/flowSteps";
import {
buildTemplateReviewHref,
TEMPLATES_FACET_RECOMMEND_QUERY,
TEMPLATES_FACET_RECOMMEND_VALUE,
} from "../../(app)/create/utils/flowSteps";
import { useTranslation } from "../../contexts/MessagesContext";
import { useTemplatesFacetGridEntries } from "./useTemplatesFacetGridEntries";
export interface TemplatesPageClientProps {
initialGridEntries: TemplateGridCardEntry[];
@@ -44,9 +49,16 @@ export default function TemplatesPageClient({
{/* Suspense boundary required by `useSearchParams` below
(Next.js 15+ static-generation contract). */}
<Suspense
fallback={<TemplatesGrid entries={initialGridEntries} fromFlow={false} />}
fallback={
<TemplatesGrid
entries={initialGridEntries}
fromFlow={false}
/>
}
>
<TemplatesGridWithSearchParams entries={initialGridEntries} />
<TemplatesGridWithSearchParams
initialGridEntries={initialGridEntries}
/>
</Suspense>
</div>
</div>
@@ -55,18 +67,25 @@ export default function TemplatesPageClient({
}
/**
* Reads `fromFlow=1` off the URL so we can skip the fresh-slate clear when
* the user arrived from `/create/review`'s "Create from template" button.
* That button pushes `/templates?fromFlow=1` so their in-progress community
* stage is preserved when they pick a template here.
* - `fromFlow=1` — skip `prepareFreshCreateFlowEntry` on template click
* (draft preserved). Used by reviewCreate from template” and profile.
* - `recommendTemplates=1` (with review only) — rank templates + “RECOMMENDED”
* from `GET /api/templates?facet.*` using the persisted community draft.
*/
function TemplatesGridWithSearchParams({
entries,
initialGridEntries,
}: {
entries: TemplateGridCardEntry[];
initialGridEntries: TemplateGridCardEntry[];
}) {
const searchParams = useSearchParams();
const fromFlow = searchParams.get("fromFlow") === "1";
const enableFacetRecommendations =
searchParams.get(TEMPLATES_FACET_RECOMMEND_QUERY) ===
TEMPLATES_FACET_RECOMMEND_VALUE;
const entries = useTemplatesFacetGridEntries({
initialGridEntries,
enableFacetRecommendations,
});
return <TemplatesGrid entries={entries} fromFlow={fromFlow} />;
}
@@ -0,0 +1,70 @@
"use client";
import { useEffect, useState } from "react";
import { readAnonymousCreateFlowState } from "../../(app)/create/utils/anonymousDraftStorage";
import { buildFacetQueryString } from "../../../lib/create/buildFacetQueryString";
import {
fetchRankedTemplatesByFacets,
isTemplatesFetchAborted,
} from "../../../lib/create/fetchTemplates";
import {
gridEntriesWithFacetScores,
type TemplateGridCardEntry,
} from "../../../lib/templates/templateGridPresentation";
type UseTemplatesFacetGridEntriesArgs = {
initialGridEntries: TemplateGridCardEntry[];
enableFacetRecommendations: boolean;
};
/**
* When `enableFacetRecommendations` (review → “Create from template” only),
* re-fetch ranked templates from `GET /api/templates?facet.*` using the
* persisted create-flow draft. Otherwise returns `initialGridEntries` from SSR.
*/
export function useTemplatesFacetGridEntries({
initialGridEntries,
enableFacetRecommendations,
}: UseTemplatesFacetGridEntriesArgs): TemplateGridCardEntry[] {
const [entries, setEntries] = useState(initialGridEntries);
useEffect(() => {
if (!enableFacetRecommendations) {
setEntries(initialGridEntries);
return;
}
const state = readAnonymousCreateFlowState();
const facetQuery = buildFacetQueryString(state);
if (facetQuery.length === 0) {
setEntries(initialGridEntries);
return;
}
const ac = new AbortController();
void (async () => {
try {
const result = await fetchRankedTemplatesByFacets({
signal: ac.signal,
facetQuery,
});
if (ac.signal.aborted) return;
if ("error" in result) {
setEntries(initialGridEntries);
return;
}
setEntries(
gridEntriesWithFacetScores(result.templates, result.scores),
);
} catch (e) {
if (isTemplatesFetchAborted(e)) return;
setEntries(initialGridEntries);
}
})();
return () => {
ac.abort();
};
}, [enableFacetRecommendations, initialGridEntries]);
return entries;
}