Files
community-rule/app/(app)/create/hooks/useCreateFlowNavigation.ts
T

176 lines
5.5 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.
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useLayoutEffect, useMemo } from "react";
import { useCreateFlow } from "../context/CreateFlowContext";
import type { CreateFlowStep } from "../types";
import {
type CreateFlowNavigationOptions,
type CreateFlowReviewReturnTarget,
CREATE_FLOW_REVIEW_RETURN_QUERY_KEY,
buildTemplateReviewHref,
getNextStep,
getPreviousStep,
parseCreateFlowScreenFromPathname,
resolveCreateFlowBackTarget,
TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY,
TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE,
} from "../utils/flowSteps";
/**
* Options passed to navigation handlers (e.g. for blur before navigate)
*/
const blurActiveElement = (): void => {
if (
typeof document !== "undefined" &&
document.activeElement instanceof HTMLElement
) {
document.activeElement.blur();
}
};
/**
* Hook for Create Rule Flow navigation.
*
* Resolves the active step from `/create/{screenId}` via
* {@link parseCreateFlowScreenFromPathname} (flowSteps). Footer Back uses
* {@link resolveCreateFlowBackTarget}: template **Use without changes** that
* still needs identity (direct from a template) walks name / description /
* photo, then Back from `community-name` returns to
* `/create/review-template/{slug}`. In-flow picks (`?fromFlow=1`) skip
* identity and land on `confirm-stakeholders`; Back from that step returns
* to the template instead of `conflict-management`.
*
* Template review footer Back uses {@link buildTemplateReviewHref}s
* `?fromFlow=1` marker (and persisted `templateReviewEntryFromCreateFlow`) so
* users who came from `/create/review` via `/create/templates` return there
* instead of `/`.
*/
export function useCreateFlowNavigation(
options?: CreateFlowNavigationOptions,
): {
currentStep: CreateFlowStep | null;
goToNextStep: () => void;
goToPreviousStep: () => void;
goToStep: (
_step: CreateFlowStep,
_navOpts?: { reviewReturn?: CreateFlowReviewReturnTarget },
) => void;
canGoNext: () => boolean;
canGoBack: () => boolean;
nextStep: CreateFlowStep | null;
previousStep: CreateFlowStep | null;
/** On `/create/review-template/…`, footer Back should go to `/create/review`. */
templateReviewFooterBackToCreateReview: boolean;
} {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();
const { state, updateState } = useCreateFlow();
const validStep = parseCreateFlowScreenFromPathname(pathname ?? null);
useLayoutEffect(() => {
if (!pathname?.includes("/create/review-template/")) return;
if (
searchParams.get(TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY) !==
TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE
) {
return;
}
if (state.templateReviewEntryFromCreateFlow === true) return;
updateState({ templateReviewEntryFromCreateFlow: true });
}, [
pathname,
searchParams,
state.templateReviewEntryFromCreateFlow,
updateState,
]);
const identityOnly =
state.pendingTemplateAction?.mode === "useWithoutChanges";
const navigationOptions = useMemo(
(): CreateFlowNavigationOptions => ({
skipCommunitySave: options?.skipCommunitySave,
useWithoutChangesIdentityOnly: identityOnly,
}),
[options?.skipCommunitySave, identityOnly],
);
const nextStep = getNextStep(validStep, navigationOptions);
const previousStep = getPreviousStep(validStep, navigationOptions);
const backTarget = useMemo(
() =>
resolveCreateFlowBackTarget(
validStep,
navigationOptions,
state.templateReviewBackSlug,
),
[validStep, navigationOptions, state.templateReviewBackSlug],
);
const goToNextStep = useCallback(() => {
blurActiveElement();
if (nextStep) {
router.push(`/create/${nextStep}`);
}
}, [router, nextStep]);
const goToPreviousStep = useCallback(() => {
blurActiveElement();
if (!backTarget) return;
if (backTarget.kind === "templateReview") {
router.push(
buildTemplateReviewHref(backTarget.slug, {
fromCreateWizard: state.templateReviewEntryFromCreateFlow === true,
}),
);
return;
}
router.push(`/create/${backTarget.step}`);
}, [router, backTarget, state.templateReviewEntryFromCreateFlow]);
const templateReviewFooterBackToCreateReview = useMemo(
() =>
Boolean(state.templateReviewEntryFromCreateFlow) ||
(pathname?.includes("/create/review-template/") &&
searchParams.get(TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY) ===
TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE),
[state.templateReviewEntryFromCreateFlow, pathname, searchParams],
);
const goToStep = useCallback(
(
step: CreateFlowStep,
navOpts?: { reviewReturn?: CreateFlowReviewReturnTarget },
) => {
blurActiveElement();
const params = new URLSearchParams(searchParams?.toString() ?? "");
if (navOpts?.reviewReturn != null) {
params.set(CREATE_FLOW_REVIEW_RETURN_QUERY_KEY, navOpts.reviewReturn);
} else {
params.delete(CREATE_FLOW_REVIEW_RETURN_QUERY_KEY);
}
const qs = params.toString();
router.push(qs.length > 0 ? `/create/${step}?${qs}` : `/create/${step}`);
},
[router, searchParams],
);
const canGoNext = useCallback(() => nextStep !== null, [nextStep]);
const canGoBack = useCallback(() => backTarget != null, [backTarget]);
return {
currentStep: validStep,
goToNextStep,
goToPreviousStep,
goToStep,
canGoNext,
canGoBack,
nextStep,
previousStep,
templateReviewFooterBackToCreateReview,
};
}