Wire Publish rule from create flow

This commit is contained in:
adilallo
2026-04-07 22:26:25 -06:00
parent a4f0b449b6
commit 8f932e95cd
16 changed files with 839 additions and 252 deletions
+285
View File
@@ -0,0 +1,285 @@
"use client";
import {
Suspense,
useCallback,
useEffect,
useState,
type ReactNode,
} from "react";
import { usePathname, useRouter } from "next/navigation";
import { CreateFlowProvider, useCreateFlow } from "./context/CreateFlowContext";
import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation";
import { useCreateFlowExit } from "./hooks/useCreateFlowExit";
import CreateFlowTopNav from "../components/utility/CreateFlowTopNav";
import { getStepIndex } from "./utils/flowSteps";
import CreateFlowFooter from "../components/utility/CreateFlowFooter";
import Button from "../components/buttons/Button";
import { buildPublishPayload } from "../../lib/create/buildPublishPayload";
import { fetchAuthSession, publishRule } from "../../lib/create/api";
import { writeLastPublishedRule } from "../../lib/create/lastPublishedRule";
import messages from "../../messages/en/index";
import { useAuthModal } from "../contexts/AuthModalContext";
import { PostLoginDraftTransfer } from "./PostLoginDraftTransfer";
import { SignedInDraftHydration } from "./SignedInDraftHydration";
import Alert from "../components/modals/Alert";
import {
CreateFlowDraftSaveBannerProvider,
useCreateFlowDraftSaveBanner,
} from "./context/CreateFlowDraftSaveBannerContext";
/** First step where Save & Exit is offered (after informational + name / `text`). */
const SAVE_EXIT_FROM_STEP_INDEX = getStepIndex("select");
function CreateFlowSessionShell({ children }: { children: ReactNode }) {
const [sessionUser, setSessionUser] = useState<
{ id: string; email: string } | null | undefined
>(undefined);
useEffect(() => {
let cancelled = false;
void fetchAuthSession().then(({ user }) => {
if (!cancelled) setSessionUser(user);
});
return () => {
cancelled = true;
};
}, []);
const sessionResolved = sessionUser !== undefined;
const enableAnonymousPersistence = sessionResolved && sessionUser === null;
return (
<CreateFlowProvider enableAnonymousPersistence={enableAnonymousPersistence}>
<CreateFlowDraftSaveBannerProvider>
<CreateFlowLayoutContent
sessionUser={sessionUser}
sessionResolved={sessionResolved}
>
{children}
</CreateFlowLayoutContent>
</CreateFlowDraftSaveBannerProvider>
</CreateFlowProvider>
);
}
function CreateFlowLayoutContent({
children,
sessionUser,
sessionResolved,
}: {
children: ReactNode;
sessionUser: { id: string; email: string } | null | undefined;
sessionResolved: boolean;
}) {
const router = useRouter();
const pathname = usePathname();
const { openLogin } = useAuthModal();
const {
currentStep,
nextStep,
previousStep,
goToNextStep,
goToPreviousStep,
} = useCreateFlowNavigation();
const { state, clearState } = useCreateFlow();
const { draftSaveBannerMessage, setDraftSaveBannerMessage } =
useCreateFlowDraftSaveBanner();
const [publishBannerMessage, setPublishBannerMessage] = useState<
string | null
>(null);
const [isPublishing, setIsPublishing] = useState(false);
const handleFinalize = useCallback(async () => {
setPublishBannerMessage(null);
const payloadResult = buildPublishPayload(state);
if (payloadResult.ok === false) {
setPublishBannerMessage(
payloadResult.error === "missingCommunityName"
? messages.create.publish.missingCommunityName
: payloadResult.error,
);
return;
}
const { title, summary, document: ruleDocument } = payloadResult;
setIsPublishing(true);
const publishResult = await publishRule({
title,
summary,
document: ruleDocument,
});
setIsPublishing(false);
if (publishResult.ok === true) {
writeLastPublishedRule({
id: publishResult.id,
title,
summary: summary ?? null,
document: ruleDocument,
});
router.push("/create/completed");
return;
}
if (publishResult.status === 401) {
openLogin({
variant: "default",
nextPath: "/create/final-review?syncDraft=1",
backdropVariant: "blurredYellow",
});
return;
}
setPublishBannerMessage(
publishResult.error.trim() !== ""
? publishResult.error
: messages.create.publish.genericPublishFailed,
);
}, [state, router, openLogin]);
const runAuthenticatedExit = useCreateFlowExit({
state,
currentStep,
clearState,
router,
user: sessionUser ?? null,
setDraftSaveBannerMessage,
});
const handleExit = async (opts?: { saveDraft?: boolean }) => {
const saveDraft = opts?.saveDraft ?? false;
if (!sessionResolved) return;
if (sessionUser === null) {
if (saveDraft) return;
openLogin({
variant: "saveProgress",
nextPath: `${pathname ?? "/create/informational"}?syncDraft=1`,
backdropVariant: "blurredYellow",
});
return;
}
if (!sessionUser) return;
await runAuthenticatedExit(opts);
};
const isCompletedStep = currentStep === "completed";
const isRightRailStep = currentStep === "right-rail";
const useFullHeightMain = isCompletedStep || isRightRailStep;
const stepIdx = currentStep != null ? getStepIndex(currentStep) : -1;
const saveDraftOnExit =
Boolean(sessionUser) && stepIdx >= SAVE_EXIT_FROM_STEP_INDEX;
const hasErrorOverlays =
Boolean(draftSaveBannerMessage) || Boolean(publishBannerMessage);
return (
<div className="relative flex h-screen min-h-0 flex-col overflow-hidden bg-black">
{hasErrorOverlays ? (
<div
className="pointer-events-none fixed left-0 right-0 top-0 z-[200] flex flex-col gap-2 px-[var(--spacing-measures-spacing-500,20px)] pt-[var(--spacing-measures-spacing-300,12px)] md:px-[var(--measures-spacing-1800,64px)]"
aria-live="polite"
>
{draftSaveBannerMessage ? (
<div className="pointer-events-auto mx-auto w-full max-w-[960px]">
<Alert
type="banner"
status="danger"
title={messages.create.topNav.draftSaveBannerTitle}
description={draftSaveBannerMessage}
onClose={() => setDraftSaveBannerMessage(null)}
className="w-full"
/>
</div>
) : null}
{publishBannerMessage ? (
<div className="pointer-events-auto mx-auto w-full max-w-[960px]">
<Alert
type="banner"
status="danger"
title={messages.create.publish.finalizeBannerTitle}
description={publishBannerMessage}
onClose={() => setPublishBannerMessage(null)}
className="w-full"
/>
</div>
) : null}
</div>
) : null}
<Suspense fallback={null}>
<SignedInDraftHydration
sessionUser={sessionUser}
sessionResolved={sessionResolved}
/>
</Suspense>
<Suspense fallback={null}>
<PostLoginDraftTransfer sessionUser={sessionUser} />
</Suspense>
<CreateFlowTopNav
hasShare={isCompletedStep}
hasExport={isCompletedStep}
hasEdit={isCompletedStep}
saveDraftOnExit={saveDraftOnExit}
onEdit={
isCompletedStep
? () => router.push("/create/final-review")
: undefined
}
onExit={(opts) => void handleExit(opts)}
buttonPalette={isCompletedStep ? "inverse" : undefined}
className={`shrink-0 ${
isCompletedStep ? "!bg-[var(--color-teal-teal50,#c9fef9)]" : ""
}`.trim()}
/>
<main
className={`flex min-h-0 flex-1 justify-center ${
useFullHeightMain
? isCompletedStep
? "items-stretch overflow-y-auto sm:overflow-hidden"
: "items-stretch overflow-hidden"
: "flex-row items-center justify-center overflow-y-auto"
}`}
>
{children}
</main>
{!isCompletedStep && (
<CreateFlowFooter
className="shrink-0"
secondButton={
nextStep ? (
<Button
buttonType="filled"
palette="default"
size="xsmall"
disabled={isPublishing}
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
onClick={() => {
if (currentStep === "final-review") {
void handleFinalize();
} else {
goToNextStep();
}
}}
>
{currentStep === "final-review"
? isPublishing
? messages.create.publish.finalizeButtonPublishing
: "Finalize CommunityRule"
: currentStep === "confirm-stakeholders"
? "Confirm Stakeholders"
: "Next"}
</Button>
) : null
}
onBackClick={previousStep ? goToPreviousStep : undefined}
/>
)}
</div>
);
}
export default function CreateFlowLayoutClient({
children,
}: {
children: ReactNode;
}) {
return <CreateFlowSessionShell>{children}</CreateFlowSessionShell>;
}
+26
View File
@@ -0,0 +1,26 @@
"use client";
import dynamic from "next/dynamic";
import type { ReactNode } from "react";
const CreateFlowLayoutClient = dynamic(
() => import("./CreateFlowLayoutClient"),
{
ssr: false,
loading: () => (
<div
className="flex h-screen min-h-0 flex-col overflow-hidden bg-black"
aria-busy="true"
aria-label="Loading create flow"
/>
),
},
);
export default function CreateFlowLayoutGate({
children,
}: {
children: ReactNode;
}) {
return <CreateFlowLayoutClient>{children}</CreateFlowLayoutClient>;
}
+41 -13
View File
@@ -16,7 +16,8 @@ const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true";
/**
* After magic-link verify, redirects to `/create/...?syncDraft=1` with session cookie.
* Uploads anonymous localStorage draft to `RuleDraft` once, then hydrates context.
* With backend sync: PUT draft once then hydrates context. Without sync: hydrates from
* `create-flow-anonymous` localStorage only (no server write).
*/
export function PostLoginDraftTransfer({
sessionUser,
@@ -38,19 +39,46 @@ export function PostLoginDraftTransfer({
if (attemptedRef.current) return;
if (!SYNC_ENABLED) {
if (attemptedRef.current) return;
attemptedRef.current = true;
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync-off path: show one-shot error then strip query
setTransferError(
"Saving to your account is not available (server sync is disabled). Your progress stays on this device.",
);
if (pathname) {
const params = new URLSearchParams(searchParams.toString());
params.delete("syncDraft");
const q = params.toString();
router.replace(q ? `${pathname}?${q}` : pathname);
}
return;
let cancelled = false;
void (async () => {
const local = readAnonymousCreateFlowState();
const pending = hasTransferPendingFlag();
if (Object.keys(local).length === 0 && !pending) {
const params = new URLSearchParams(searchParams.toString());
params.delete("syncDraft");
const q = params.toString();
if (pathname) {
router.replace(q ? `${pathname}?${q}` : pathname);
}
attemptedRef.current = false;
return;
}
const segment = pathname?.split("/").pop() ?? "";
const step = isValidStep(segment) ? segment : undefined;
const payload = {
...local,
...(step ? { currentStep: step } : {}),
};
if (cancelled) return;
clearAnonymousCreateFlowStorage();
replaceState(payload);
if (cancelled) return;
if (pathname) {
const params = new URLSearchParams(searchParams.toString());
params.delete("syncDraft");
const q = params.toString();
router.replace(q ? `${pathname}?${q}` : pathname);
}
})();
return () => {
cancelled = true;
};
}
attemptedRef.current = true;
+29 -8
View File
@@ -6,9 +6,12 @@ import HeaderLockup from "../../components/type/HeaderLockup";
import CommunityRuleDocument from "../../components/sections/CommunityRuleDocument";
import type { CommunityRuleDocumentSection } from "../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types";
import Alert from "../../components/modals/Alert";
import { parseDocumentSectionsForDisplay } from "../../../lib/create/buildPublishPayload";
import { readLastPublishedRule } from "../../../lib/create/lastPublishedRule";
const TITLE = "Mutual Aid Mondays";
const DESCRIPTION =
/** Demo copy when `/create/completed` is opened without a prior publish in this tab. */
const FALLBACK_TITLE = "Mutual Aid Mondays";
const FALLBACK_DESCRIPTION =
"Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness.";
const TOAST_TITLE = "This is what folks see when you share your CommunityRule";
@@ -91,6 +94,12 @@ const COMPLETED_RULE_SECTIONS: CommunityRuleDocumentSection[] = [
export default function CompletedPage() {
const [isMounted, setIsMounted] = useState(false);
const [toastDismissed, setToastDismissed] = useState(false);
const [headerTitle, setHeaderTitle] = useState(FALLBACK_TITLE);
const [headerDescription, setHeaderDescription] = useState<
string | undefined
>(FALLBACK_DESCRIPTION);
const [documentSections, setDocumentSections] =
useState<CommunityRuleDocumentSection[]>(COMPLETED_RULE_SECTIONS);
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
useEffect(() => {
@@ -98,6 +107,18 @@ export default function CompletedPage() {
setIsMounted(true);
}, []);
useEffect(() => {
const stored = readLastPublishedRule();
if (!stored) return;
const parsed = parseDocumentSectionsForDisplay(stored.document);
if (parsed.length === 0) return;
setDocumentSections(parsed);
setHeaderTitle(stored.title);
const sum =
typeof stored.summary === "string" ? stored.summary.trim() : "";
setHeaderDescription(sum.length > 0 ? sum : undefined);
}, []);
const showDesktopLayout = !isMounted || isMdOrLarger;
if (showDesktopLayout) {
@@ -108,8 +129,8 @@ export default function CompletedPage() {
{/* Left column: community title + header, centered, does not scroll */}
<div className="flex min-w-0 flex-col justify-center overflow-hidden py-8">
<HeaderLockup
title={TITLE}
description={DESCRIPTION}
title={headerTitle}
description={headerDescription}
justification="left"
size="L"
palette="inverse"
@@ -124,7 +145,7 @@ export default function CompletedPage() {
/>
<div className="py-8 min-w-0">
<CommunityRuleDocument
sections={COMPLETED_RULE_SECTIONS}
sections={documentSections}
className="min-w-0"
/>
</div>
@@ -159,14 +180,14 @@ export default function CompletedPage() {
<div className="w-full flex flex-col items-center px-5 min-w-0 bg-[var(--color-teal-teal50,#c9fef9)] py-8">
<div className="flex flex-col gap-4 w-full max-w-[639px]">
<HeaderLockup
title={TITLE}
description={DESCRIPTION}
title={headerTitle}
description={headerDescription}
justification="left"
size="M"
palette="inverse"
/>
<CommunityRuleDocument
sections={COMPLETED_RULE_SECTIONS}
sections={documentSections}
useCardStyle
className="w-full p-4"
/>
+22 -10
View File
@@ -1,18 +1,19 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import HeaderLockup from "../../components/type/HeaderLockup";
import RuleCard from "../../components/cards/RuleCard";
import type { Category } from "../../components/cards/RuleCard/RuleCard.types";
import { useCreateFlow } from "../context/CreateFlowContext";
const TITLE = "Review your CommunityRule";
const DESCRIPTION =
"Here's what other people will see. Make sure everything looks good before you finalize everything. Once the rule is finalized, you must use one of your decision-making mechanisms to edit it again.";
const RULE_CARD_TITLE = "Mutual Aid Mondays";
const RULE_CARD_DESCRIPTION =
"Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness.";
const RULE_CARD_TITLE_FALLBACK = "Your community";
const RULE_CARD_DESCRIPTION_FALLBACK =
"Add a short description of your community on earlier steps when that field is available. For now, this card shows your community name.";
/** Static categories for final review (read-only display). */
const FINAL_REVIEW_CATEGORIES: Category[] = [
@@ -55,9 +56,20 @@ const FINAL_REVIEW_CATEGORIES: Category[] = [
* Figma: 20907-212767 (full-size), 20976-220705 (small breakpoint).
*/
export default function FinalReviewPage() {
const { state } = useCreateFlow();
const [isMounted, setIsMounted] = useState(false);
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
const ruleCardTitle = useMemo(() => {
const t = typeof state.title === "string" ? state.title.trim() : "";
return t.length > 0 ? t : RULE_CARD_TITLE_FALLBACK;
}, [state.title]);
const ruleCardDescription = useMemo(() => {
const s = typeof state.summary === "string" ? state.summary.trim() : "";
return s.length > 0 ? s : RULE_CARD_DESCRIPTION_FALLBACK;
}, [state.summary]);
// Avoid flash: only use breakpoint after mount so SSR and first paint use same layout (desktop).
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer layout breakpoint until after mount to prevent flash
@@ -80,13 +92,13 @@ export default function FinalReviewPage() {
</div>
<div className="min-w-0 w-full flex flex-col items-stretch">
<RuleCard
title={RULE_CARD_TITLE}
description={RULE_CARD_DESCRIPTION}
title={ruleCardTitle}
description={ruleCardDescription}
size="L"
expanded={true}
backgroundColor="bg-[#c9fef9]"
logoUrl="/assets/Vector_MutualAid.svg"
logoAlt={RULE_CARD_TITLE}
logoAlt={ruleCardTitle}
categories={FINAL_REVIEW_CATEGORIES}
className="rounded-[24px] !max-w-full !w-full min-w-0"
onClick={() => {}}
@@ -107,13 +119,13 @@ export default function FinalReviewPage() {
size="M"
/>
<RuleCard
title={RULE_CARD_TITLE}
description={RULE_CARD_DESCRIPTION}
title={ruleCardTitle}
description={ruleCardDescription}
size="L"
expanded={true}
backgroundColor="bg-[#c9fef9]"
logoUrl="/assets/Vector_MutualAid.svg"
logoAlt={RULE_CARD_TITLE}
logoAlt={ruleCardTitle}
categories={FINAL_REVIEW_CATEGORIES}
className="w-full rounded-[12px] p-4"
onClick={() => {}}
+4 -191
View File
@@ -1,193 +1,6 @@
"use client";
import type { ReactNode } from "react";
import CreateFlowLayoutGate from "./CreateFlowLayoutGate";
import { Suspense, useEffect, useState, type ReactNode } from "react";
import { usePathname, useRouter } from "next/navigation";
import { CreateFlowProvider, useCreateFlow } from "./context/CreateFlowContext";
import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation";
import { useCreateFlowExit } from "./hooks/useCreateFlowExit";
import CreateFlowTopNav from "../components/utility/CreateFlowTopNav";
import { getStepIndex } from "./utils/flowSteps";
import CreateFlowFooter from "../components/utility/CreateFlowFooter";
import Button from "../components/buttons/Button";
import { fetchAuthSession } from "../../lib/create/api";
import messages from "../../messages/en/index";
import { useAuthModal } from "../contexts/AuthModalContext";
import { PostLoginDraftTransfer } from "./PostLoginDraftTransfer";
import { SignedInDraftHydration } from "./SignedInDraftHydration";
import Alert from "../components/modals/Alert";
import {
CreateFlowDraftSaveBannerProvider,
useCreateFlowDraftSaveBanner,
} from "./context/CreateFlowDraftSaveBannerContext";
/** First step where Save & Exit is offered (after informational + name / `text`). */
const SAVE_EXIT_FROM_STEP_INDEX = getStepIndex("select");
function CreateFlowSessionShell({ children }: { children: ReactNode }) {
const [sessionUser, setSessionUser] = useState<
{ id: string; email: string } | null | undefined
>(undefined);
useEffect(() => {
let cancelled = false;
void fetchAuthSession().then(({ user }) => {
if (!cancelled) setSessionUser(user);
});
return () => {
cancelled = true;
};
}, []);
const sessionResolved = sessionUser !== undefined;
const enableAnonymousPersistence = sessionResolved && sessionUser === null;
return (
<CreateFlowProvider enableAnonymousPersistence={enableAnonymousPersistence}>
<CreateFlowDraftSaveBannerProvider>
<CreateFlowLayoutContent
sessionUser={sessionUser}
sessionResolved={sessionResolved}
>
{children}
</CreateFlowLayoutContent>
</CreateFlowDraftSaveBannerProvider>
</CreateFlowProvider>
);
}
function CreateFlowLayoutContent({
children,
sessionUser,
sessionResolved,
}: {
children: ReactNode;
sessionUser: { id: string; email: string } | null | undefined;
sessionResolved: boolean;
}) {
const router = useRouter();
const pathname = usePathname();
const { openLogin } = useAuthModal();
const {
currentStep,
nextStep,
previousStep,
goToNextStep,
goToPreviousStep,
} = useCreateFlowNavigation();
const { state, clearState } = useCreateFlow();
const { draftSaveBannerMessage, setDraftSaveBannerMessage } =
useCreateFlowDraftSaveBanner();
const runAuthenticatedExit = useCreateFlowExit({
state,
currentStep,
clearState,
router,
user: sessionUser ?? null,
setDraftSaveBannerMessage,
});
const handleExit = async (opts?: { saveDraft?: boolean }) => {
const saveDraft = opts?.saveDraft ?? false;
if (!sessionResolved) return;
if (sessionUser === null) {
if (saveDraft) return;
openLogin({
variant: "saveProgress",
nextPath: `${pathname ?? "/create/informational"}?syncDraft=1`,
backdropVariant: "blurredYellow",
});
return;
}
if (!sessionUser) return;
await runAuthenticatedExit(opts);
};
const isCompletedStep = currentStep === "completed";
const isRightRailStep = currentStep === "right-rail";
const useFullHeightMain = isCompletedStep || isRightRailStep;
const stepIdx = currentStep != null ? getStepIndex(currentStep) : -1;
const saveDraftOnExit =
Boolean(sessionUser) && stepIdx >= SAVE_EXIT_FROM_STEP_INDEX;
return (
<div
className={`bg-black flex flex-col ${useFullHeightMain ? "h-screen overflow-hidden" : "min-h-screen"}`}
>
{draftSaveBannerMessage ? (
<div className="w-full shrink-0 px-[var(--spacing-measures-spacing-500,20px)] pt-[var(--spacing-measures-spacing-300,12px)] md:px-[var(--measures-spacing-1800,64px)] z-[100]">
<Alert
type="banner"
status="danger"
title={messages.create.topNav.draftSaveBannerTitle}
description={draftSaveBannerMessage}
onClose={() => setDraftSaveBannerMessage(null)}
className="w-full max-w-[960px] mx-auto"
/>
</div>
) : null}
<Suspense fallback={null}>
<SignedInDraftHydration
sessionUser={sessionUser}
sessionResolved={sessionResolved}
/>
</Suspense>
<Suspense fallback={null}>
<PostLoginDraftTransfer sessionUser={sessionUser} />
</Suspense>
<CreateFlowTopNav
hasShare={isCompletedStep}
hasExport={isCompletedStep}
hasEdit={isCompletedStep}
saveDraftOnExit={saveDraftOnExit}
onEdit={
isCompletedStep
? () => router.push("/create/final-review")
: undefined
}
onExit={(opts) => void handleExit(opts)}
buttonPalette={isCompletedStep ? "inverse" : undefined}
className={
isCompletedStep ? "!bg-[var(--color-teal-teal50,#c9fef9)]" : undefined
}
/>
<main
className={`flex-1 flex min-h-0 justify-center ${useFullHeightMain ? "items-stretch overflow-hidden" : "items-center overflow-auto"}`}
>
{children}
</main>
{!isCompletedStep && (
<CreateFlowFooter
secondButton={
nextStep ? (
<Button
buttonType="filled"
palette="default"
size="xsmall"
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
onClick={goToNextStep}
>
{currentStep === "final-review"
? "Finalize CommunityRule"
: currentStep === "confirm-stakeholders"
? "Confirm Stakeholders"
: "Next"}
</Button>
) : null
}
onBackClick={previousStep ? goToPreviousStep : undefined}
/>
)}
</div>
);
}
export default function CreateFlowLayout({
children,
}: {
children: ReactNode;
}) {
return <CreateFlowSessionShell>{children}</CreateFlowSessionShell>;
export default function CreateFlowLayout({ children }: { children: ReactNode }) {
return <CreateFlowLayoutGate>{children}</CreateFlowLayoutGate>;
}