Keep the template picker in the create flow, make catalog cards real links, and stop showing a fake community when review has no draft.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -27,8 +27,7 @@ import {
|
||||
shouldOfferCreateFlowSaveAndExit,
|
||||
isDirectTemplateReviewEntry,
|
||||
createFlowStepUsesSelectSplitScroll,
|
||||
TEMPLATES_FACET_RECOMMEND_QUERY,
|
||||
TEMPLATES_FACET_RECOMMEND_VALUE,
|
||||
isCreateFlowTemplatesPickerPath,
|
||||
TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY,
|
||||
TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE,
|
||||
} from "./utils/flowSteps";
|
||||
@@ -309,6 +308,8 @@ function CreateFlowLayoutContent({
|
||||
fromCreateWizard,
|
||||
markCreateFlowInteraction,
|
||||
});
|
||||
const isCreateTemplatesPickerRoute =
|
||||
isCreateFlowTemplatesPickerPath(pathname);
|
||||
|
||||
const runAuthenticatedExit = useCreateFlowExit({
|
||||
state,
|
||||
@@ -566,9 +567,11 @@ function CreateFlowLayoutContent({
|
||||
? "items-stretch overflow-y-auto md:overflow-hidden"
|
||||
: isSelectSplitScrollStep
|
||||
? "items-start justify-start overflow-y-auto max-lg:overflow-y-auto lg:min-h-0 lg:items-stretch lg:overflow-hidden"
|
||||
: isTemplateReviewRoute || isFinalReviewLike || isCardLayoutStep
|
||||
? CREATE_FLOW_MD_CENTERED_MAIN_CLASS
|
||||
: "items-start justify-center overflow-y-auto md:items-center";
|
||||
: isCreateTemplatesPickerRoute
|
||||
? "items-start justify-start overflow-y-auto"
|
||||
: isTemplateReviewRoute || isFinalReviewLike || isCardLayoutStep
|
||||
? CREATE_FLOW_MD_CENTERED_MAIN_CLASS
|
||||
: "items-start justify-center overflow-y-auto md:items-center";
|
||||
|
||||
const isTextStep = createFlowStepUsesCenteredTextLayout(currentStep);
|
||||
const mainMaxMdJustify =
|
||||
@@ -579,10 +582,9 @@ function CreateFlowLayoutContent({
|
||||
? "max-md:flex-col max-md:items-stretch"
|
||||
: "max-md:flex-col max-md:items-center";
|
||||
const mainResponsiveLayout = `${mainMaxMdCross} ${mainMaxMdJustify} md:flex-row md:justify-center`;
|
||||
const saveDraftOnExit = shouldOfferCreateFlowSaveAndExit(
|
||||
currentStep,
|
||||
sessionUser,
|
||||
);
|
||||
const saveDraftOnExit =
|
||||
shouldOfferCreateFlowSaveAndExit(currentStep, sessionUser) ||
|
||||
isCreateTemplatesPickerRoute;
|
||||
|
||||
const proportionBarProgress = getProportionBarProgressForCreateFlowStep(
|
||||
currentStep,
|
||||
@@ -814,9 +816,11 @@ function CreateFlowLayoutContent({
|
||||
contentMaxClass={getCreateFlowContentMaxClass({
|
||||
step: currentStep,
|
||||
isTemplateReview: isTemplateReviewRoute,
|
||||
isTemplatesPicker: isCreateTemplatesPickerRoute,
|
||||
})}
|
||||
progressBar={
|
||||
!isTemplateReviewRoute &&
|
||||
!isCreateTemplatesPickerRoute &&
|
||||
!isFinalReviewLike &&
|
||||
reviewReturnTarget !== "edit-rule"
|
||||
}
|
||||
@@ -920,15 +924,7 @@ function CreateFlowLayoutContent({
|
||||
disabled={isPublishing}
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||
onClick={() => {
|
||||
// `fromFlow=1` tells `/templates` to skip the fresh-slate
|
||||
// draft clear it normally runs on template click, so the
|
||||
// user's in-progress Create Community stage survives this
|
||||
// detour. Direct entries to `/templates` (no marker) and
|
||||
// home "Popular templates" clicks always start fresh by
|
||||
// wiping anonymous draft storage at click time.
|
||||
router.push(
|
||||
`/templates?${TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY}=${TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE}&${TEMPLATES_FACET_RECOMMEND_QUERY}=${TEMPLATES_FACET_RECOMMEND_VALUE}`,
|
||||
);
|
||||
router.push(CREATE_ROUTES.templatesPicker);
|
||||
}}
|
||||
>
|
||||
{footer.createFromTemplate}
|
||||
@@ -1013,7 +1009,11 @@ function CreateFlowLayoutContent({
|
||||
) : null
|
||||
}
|
||||
onBackClick={
|
||||
isTemplateReviewRoute
|
||||
isCreateTemplatesPickerRoute
|
||||
? () => {
|
||||
router.push(CREATE_ROUTES.review);
|
||||
}
|
||||
: isTemplateReviewRoute
|
||||
? () =>
|
||||
router.push(
|
||||
templateReviewFooterBackToCreateReview
|
||||
|
||||
@@ -15,6 +15,7 @@ import { CREATE_FLOW_PAGE_GUTTER_CLASS } from "./components/createFlowLayoutToke
|
||||
import Alert from "../../components/modals/Alert";
|
||||
import {
|
||||
isValidStep,
|
||||
isCreateFlowTemplatesPickerPath,
|
||||
parseCreateFlowScreenFromPathname,
|
||||
} from "./utils/flowSteps";
|
||||
import { hasFreshEntryPending } from "./utils/prepareFreshCreateFlowEntry";
|
||||
@@ -111,6 +112,9 @@ export function SignedInDraftHydration({
|
||||
if (pathname?.includes("/create/review-template/")) {
|
||||
return;
|
||||
}
|
||||
if (isCreateFlowTemplatesPickerPath(pathname)) {
|
||||
return;
|
||||
}
|
||||
if (touchedRef.current) {
|
||||
finishedUserIdRef.current = userId;
|
||||
return;
|
||||
|
||||
@@ -71,8 +71,9 @@ export const CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS =
|
||||
export function getCreateFlowContentMaxClass(options: {
|
||||
step: CreateFlowStep | null | undefined;
|
||||
isTemplateReview?: boolean;
|
||||
isTemplatesPicker?: boolean;
|
||||
}): string {
|
||||
if (options.isTemplateReview) {
|
||||
if (options.isTemplateReview || options.isTemplatesPicker) {
|
||||
return CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS;
|
||||
}
|
||||
if (!options.step) {
|
||||
|
||||
@@ -43,7 +43,8 @@ const blurActiveElement = (): void => {
|
||||
*
|
||||
* Template review footer Back uses {@link buildTemplateReviewHref}’s
|
||||
* `?fromFlow=1` marker (and persisted `templateReviewEntryFromCreateFlow`) so
|
||||
* users who came from `/create/review` return there instead of `/`.
|
||||
* users who came from `/create/review` via `/create/templates` return there
|
||||
* instead of `/`.
|
||||
*/
|
||||
export function useCreateFlowNavigation(
|
||||
options?: CreateFlowNavigationOptions,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Rule from "../../../../components/cards/Rule";
|
||||
import Button from "../../../../components/buttons/Button";
|
||||
import { useTranslation } from "../../../../contexts/MessagesContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
vectorMarkPath,
|
||||
} from "../../../../../lib/assetUtils";
|
||||
import { methodSectionsPinsForHydratedSelections } from "../../../../../lib/create/publishedDocumentToCreateFlowState";
|
||||
import { createFlowStepPath } from "../../utils/createFlowPaths";
|
||||
import { CREATE_ROUTES, createFlowStepPath } from "../../utils/createFlowPaths";
|
||||
|
||||
/** Create Community review — Figma `19706:12135` (`/create/review`; two columns from `lg:`; column caps in `createFlowLayoutTokens`). */
|
||||
export function CommunityReviewScreen() {
|
||||
@@ -25,6 +26,15 @@ export function CommunityReviewScreen() {
|
||||
const lgUp = useCreateFlowLgUp();
|
||||
const t = useTranslation("create.community.review");
|
||||
const { state, updateState } = useCreateFlow();
|
||||
/**
|
||||
* Server layout has an empty context; the client layout may already hold a
|
||||
* named draft. Defer empty-vs-congrats until after mount so the first paint
|
||||
* matches SSR (`null`) instead of swapping HeaderLockup titles.
|
||||
*/
|
||||
const [reviewReady, setReviewReady] = useState(false);
|
||||
useEffect(() => {
|
||||
setReviewReady(true);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* If the user picked **Customize** from a template before finishing community
|
||||
@@ -70,11 +80,10 @@ export function CommunityReviewScreen() {
|
||||
const cardTitle =
|
||||
typeof state.title === "string" && state.title.trim().length > 0
|
||||
? state.title.trim()
|
||||
: t("ruleCard.title");
|
||||
: "";
|
||||
/**
|
||||
* No placeholder fallback: if the user skipped `community-context`, leave
|
||||
* the card description off rather than render the old "Mutual Aid Monday
|
||||
* is a grassroots community…" sample, which read as real user copy.
|
||||
* the card description off rather than render sample copy as real user data.
|
||||
*/
|
||||
const cardDescription =
|
||||
typeof state.communityContext === "string" &&
|
||||
@@ -88,6 +97,36 @@ export function CommunityReviewScreen() {
|
||||
? state.communityAvatarUrl.trim()
|
||||
: null;
|
||||
|
||||
if (state.pendingTemplateAction || !reviewReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!cardTitle) {
|
||||
return (
|
||||
<CreateFlowStepShell
|
||||
variant="centeredNarrow"
|
||||
contentTopBelowMd="space-1400"
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col items-start gap-6 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||
>
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("empty.title")}
|
||||
description={t("empty.description")}
|
||||
/>
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
href={CREATE_ROUTES.createFirstStep}
|
||||
>
|
||||
{t("empty.startLabel")}
|
||||
</Button>
|
||||
</div>
|
||||
</CreateFlowStepShell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CreateFlowStepShell
|
||||
variant="wideGridLoosePadding"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { GovernanceTemplateGrid } from "../../../components/sections/GovernanceTemplateGrid";
|
||||
import type { TemplateGridCardEntry } from "../../../../lib/templates/templateGridPresentation";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useTemplatesFacetGridEntries } from "../../../(marketing)/templates/useTemplatesFacetGridEntries";
|
||||
import { CreateFlowHeaderLockup } from "../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../components/CreateFlowStepShell";
|
||||
import { CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS } from "../components/createFlowLayoutTokens";
|
||||
import { buildTemplateReviewHref } from "../utils/flowSteps";
|
||||
|
||||
export function CreateFlowTemplatesPageClient({
|
||||
initialGridEntries,
|
||||
}: {
|
||||
initialGridEntries: TemplateGridCardEntry[];
|
||||
}) {
|
||||
const t = useTranslation("pages.templates");
|
||||
const entries = useTemplatesFacetGridEntries({
|
||||
initialGridEntries,
|
||||
enableFacetRecommendations: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<CreateFlowStepShell
|
||||
variant="wideGridLoosePadding"
|
||||
contentTopBelowMd="space-1400"
|
||||
>
|
||||
<div
|
||||
className={`mx-auto flex w-full min-w-0 flex-col gap-6 pb-8 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
||||
>
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("title")}
|
||||
description={t("subtitle")}
|
||||
justification="left"
|
||||
/>
|
||||
<GovernanceTemplateGrid
|
||||
entries={entries}
|
||||
hrefForTemplate={(slug) =>
|
||||
buildTemplateReviewHref(slug, { fromCreateWizard: true })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CreateFlowStepShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
import messages from "../../../../messages/en/index";
|
||||
import { listRuleTemplatesFromDb } from "../../../../lib/server/ruleTemplates";
|
||||
import { routeMetadata } from "../../../../lib/siteMetadata";
|
||||
import { gridEntriesForFullCatalogWithFallback } from "../../../../lib/templates/templateGridPresentation";
|
||||
import { CREATE_ROUTES } from "../utils/createFlowPaths";
|
||||
import { CreateFlowTemplatesPageClient } from "./CreateFlowTemplatesPageClient";
|
||||
|
||||
export const metadata: Metadata = routeMetadata(CREATE_ROUTES.templatesPicker, {
|
||||
title: messages.metadata.templates.title,
|
||||
});
|
||||
|
||||
/** In-flow template catalog after review “Create from template”. */
|
||||
export default async function CreateFlowTemplatesPage() {
|
||||
const rows = await listRuleTemplatesFromDb();
|
||||
const initialGridEntries = gridEntriesForFullCatalogWithFallback(rows);
|
||||
return <CreateFlowTemplatesPageClient initialGridEntries={initialGridEntries} />;
|
||||
}
|
||||
@@ -215,7 +215,7 @@ export interface CreateFlowState {
|
||||
templateReviewBackSlug?: string;
|
||||
/**
|
||||
* True when the user opened `/create/review-template/{slug}` from the create
|
||||
* wizard (`/templates?fromFlow=1` after `/create/review`). Persisted so Back
|
||||
* wizard (`/create/templates` after `/create/review`). Persisted so Back
|
||||
* from template review targets `/create/review` and so returning from
|
||||
* `confirm-stakeholders` can re-apply `?fromFlow=1` on the template URL.
|
||||
*/
|
||||
|
||||
@@ -20,6 +20,8 @@ export const CREATE_ROUTES = {
|
||||
/** Direct path to the first wizard step so client navigations skip the redirect hop. */
|
||||
createFirstStep: `/create/${FIRST_STEP}`,
|
||||
review: "/create/review",
|
||||
/** In-flow template catalog (wizard chrome). Marketing catalog remains `/templates`. */
|
||||
templatesPicker: "/create/templates",
|
||||
finalReview: "/create/final-review",
|
||||
completed: "/create/completed",
|
||||
editRule: "/create/edit-rule",
|
||||
|
||||
@@ -235,18 +235,34 @@ export function parseCreateFlowScreenFromPathname(
|
||||
): CreateFlowStep | null {
|
||||
if (!pathname || pathname.length === 0) return null;
|
||||
if (pathname.includes("/create/review-template/")) return null;
|
||||
if (isCreateFlowTemplatesPickerPath(pathname)) return null;
|
||||
|
||||
const parts = pathname.split("/").filter(Boolean);
|
||||
const createIdx = parts.indexOf("create");
|
||||
if (createIdx === -1 || createIdx >= parts.length - 1) return null;
|
||||
|
||||
const segment = parts[createIdx + 1];
|
||||
if (segment === "review-template") return null;
|
||||
if (segment === "review-template" || segment === "templates") return null;
|
||||
|
||||
return isValidStep(segment) ? segment : null;
|
||||
}
|
||||
|
||||
/** Same query as `/templates?fromFlow=1` — template was picked after `/create/review`. */
|
||||
/**
|
||||
* `/create/templates` — in-flow catalog after review “Create from template”.
|
||||
* Not a wizard step; layout keeps Back / Save & Exit and does not use
|
||||
* marketing `/templates` chrome.
|
||||
*/
|
||||
export function isCreateFlowTemplatesPickerPath(
|
||||
pathname: string | null | undefined,
|
||||
): boolean {
|
||||
if (!pathname) return false;
|
||||
const parts = pathname.split("/").filter(Boolean);
|
||||
const createIdx = parts.indexOf("create");
|
||||
if (createIdx === -1 || createIdx !== parts.length - 2) return false;
|
||||
return parts[createIdx + 1] === "templates";
|
||||
}
|
||||
|
||||
/** Same query as `/create/review-template/…?fromFlow=1` — template was picked in-flow. */
|
||||
export const TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY = "fromFlow" as const;
|
||||
export const TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE = "1" as const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user