Files
community-rule/app/(app)/create/utils/flowSteps.ts
T
2026-04-29 19:24:50 -06:00

192 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Step definitions and helpers for the Create Rule Flow
*
* Single source of truth for step order and navigation helpers.
* Order matches Figma Create Community (frames 18) then later stages.
* `community-structure` precedes `community-context` and `community-size` (Figma frame 3 vs 5 swap).
*/
import type { CreateFlowStep } from "../types";
/**
* Ordered list of steps in the create rule flow
*/
export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [
"informational",
"community-name",
"community-structure",
"community-context",
"community-size",
"community-upload",
"community-save",
"review",
"core-values",
"communication-methods",
"membership-methods",
"decision-approaches",
"conflict-management",
"confirm-stakeholders",
"final-review",
"completed",
] as const;
/**
* Valid URL segments for `/create/[screenId]` (includes branch-only `edit-rule`).
* Linear order for navigation remains {@link FLOW_STEP_ORDER}.
*/
export const VALID_STEPS: readonly CreateFlowStep[] = [
...FLOW_STEP_ORDER,
"edit-rule",
] as const;
/**
* First step in the flow (entry point)
*/
export const FIRST_STEP: CreateFlowStep = FLOW_STEP_ORDER[0];
/** Options for navigation when the email / magic-link save step is not shown (signed-in users). */
export type CreateFlowNavigationOptions = {
skipCommunitySave?: boolean;
};
/**
* Returns the next step in the flow, or null if current is last/invalid
*/
export function getNextStep(
currentStep: CreateFlowStep | null | undefined,
options?: CreateFlowNavigationOptions,
): CreateFlowStep | null {
if (!currentStep) return null;
const index = FLOW_STEP_ORDER.indexOf(currentStep);
if (index === -1 || index === FLOW_STEP_ORDER.length - 1) return null;
const next = FLOW_STEP_ORDER[index + 1] as CreateFlowStep;
if (options?.skipCommunitySave && next === "community-save") {
return getNextStep("community-save", options);
}
return next;
}
/**
* Returns the previous step in the flow, or null if current is first/invalid
*/
export function getPreviousStep(
currentStep: CreateFlowStep | null | undefined,
options?: CreateFlowNavigationOptions,
): CreateFlowStep | null {
if (!currentStep) return null;
const index = FLOW_STEP_ORDER.indexOf(currentStep);
if (index <= 0) return null;
const prev = FLOW_STEP_ORDER[index - 1] as CreateFlowStep;
if (options?.skipCommunitySave && prev === "community-save") {
return getPreviousStep("community-save", options);
}
return prev;
}
/**
* Where the create-flow footer Back action should go. Usually the previous
* step in {@link FLOW_STEP_ORDER}; when the user reached `confirm-stakeholders`
* via template **Use without changes**, Back returns to template review instead
* of `conflict-management` (that segment was skipped).
*/
export type CreateFlowBackTarget =
| { kind: "step"; step: CreateFlowStep }
| { kind: "templateReview"; slug: string };
export function resolveCreateFlowBackTarget(
currentStep: CreateFlowStep | null | undefined,
options: CreateFlowNavigationOptions | undefined,
templateReviewBackSlug: string | undefined | null,
): CreateFlowBackTarget | null {
const slug =
typeof templateReviewBackSlug === "string"
? templateReviewBackSlug.trim()
: "";
if (currentStep === "confirm-stakeholders" && slug.length > 0) {
return { kind: "templateReview", slug };
}
const prev = getPreviousStep(currentStep, options);
return prev != null ? { kind: "step", step: prev } : null;
}
/**
* Returns the index of the step (0-based), or -1 if invalid
*/
export function getStepIndex(step: CreateFlowStep | null | undefined): number {
if (!step) return -1;
return FLOW_STEP_ORDER.indexOf(step);
}
/**
* Whether the given string is a valid create flow step
*/
export function isValidStep(
step: string | null | undefined,
): step is CreateFlowStep {
return (
typeof step === "string" &&
(VALID_STEPS as readonly string[]).includes(step)
);
}
/**
* Parses `/create/{screenId}` (and optional trailing segments) from pathname.
* Returns null for non-wizard paths (e.g. `/create/review-template/...`).
*/
export function parseCreateFlowScreenFromPathname(
pathname: string | null,
): CreateFlowStep | null {
if (!pathname || pathname.length === 0) return null;
if (pathname.includes("/create/review-template/")) 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;
return isValidStep(segment) ? segment : null;
}
/** Same query as `/templates?fromFlow=1` — template was picked after `/create/review`. */
export const TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY = "fromFlow" as const;
export const TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE = "1" as const;
/**
* Only set from `/create/review` “Create from template” with `fromFlow=1`.
* Enables facet-ranked `GET /api/templates` + “RECOMMENDED” on the grid; omit
* on profile and marketing so stale localStorage facets never show badges.
*/
export const TEMPLATES_FACET_RECOMMEND_QUERY = "recommendTemplates" as const;
export const TEMPLATES_FACET_RECOMMEND_VALUE = "1" as const;
/** `/create/{step}?reviewReturn=…` — set when opening a custom-rule step from final-review or edit-rule via + */
export const CREATE_FLOW_REVIEW_RETURN_QUERY_KEY = "reviewReturn" as const;
export type CreateFlowReviewReturnTarget = "final-review" | "edit-rule";
export function parseReviewReturnSearchParam(
searchParams: { get: (name: string) => string | null } | null | undefined,
): CreateFlowReviewReturnTarget | null {
if (!searchParams) return null;
const raw = searchParams.get(CREATE_FLOW_REVIEW_RETURN_QUERY_KEY);
if (raw === "final-review" || raw === "edit-rule") return raw;
return null;
}
/**
* `/create/review-template/{slug}` with optional marker so chrome can send
* footer Back to `/create/review` instead of marketing home.
*/
export function buildTemplateReviewHref(
slug: string,
options?: { fromCreateWizard?: boolean },
): string {
const path = `/create/review-template/${encodeURIComponent(slug)}`;
if (options?.fromCreateWizard) {
return `${path}?${TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY}=${TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE}`;
}
return path;
}