App reorganization
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import type { CreateFlowState, CreateFlowStep } from "../types";
|
||||
import { saveDraftToServer } from "../../../../lib/create/api";
|
||||
import messages from "../../../../messages/en/index";
|
||||
|
||||
const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true";
|
||||
|
||||
export type CreateFlowExitClearState = () => void;
|
||||
|
||||
type AppRouterLike = { push: (_href: string) => void };
|
||||
|
||||
/**
|
||||
* Leave the create flow for a **signed-in** user. Caller must not invoke for anonymous users.
|
||||
*/
|
||||
export function useCreateFlowExit({
|
||||
state,
|
||||
currentStep,
|
||||
clearState,
|
||||
router,
|
||||
user,
|
||||
setDraftSaveBannerMessage,
|
||||
}: {
|
||||
state: CreateFlowState;
|
||||
currentStep: CreateFlowStep | null;
|
||||
clearState: CreateFlowExitClearState;
|
||||
router: AppRouterLike;
|
||||
user: { id: string; email: string } | null;
|
||||
/** When save fails, surface the server message in the create shell banner (no leave confirm). */
|
||||
setDraftSaveBannerMessage?: (_message: string | null) => void;
|
||||
}): (_options?: { saveDraft?: boolean }) => Promise<void> {
|
||||
return useCallback(
|
||||
async (options?: { saveDraft?: boolean }) => {
|
||||
if (!user) return;
|
||||
|
||||
const saveDraft = options?.saveDraft ?? false;
|
||||
|
||||
if (!saveDraft && typeof window !== "undefined") {
|
||||
const confirmed = window.confirm(
|
||||
messages.create.topNav.leaveConfirmLoss,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
if (saveDraft && SYNC_ENABLED) {
|
||||
const payload: CreateFlowState = {
|
||||
...state,
|
||||
...(currentStep ? { currentStep } : {}),
|
||||
};
|
||||
const result = await saveDraftToServer(payload);
|
||||
if (result.ok === true) {
|
||||
setDraftSaveBannerMessage?.(null);
|
||||
} else {
|
||||
setDraftSaveBannerMessage?.(result.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
clearState();
|
||||
router.push("/");
|
||||
},
|
||||
[state, currentStep, clearState, router, user, setDraftSaveBannerMessage],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMediaQuery } from "../../../hooks/useMediaQuery";
|
||||
|
||||
/** `--breakpoint-lg` (1024px); same SSR/first-paint pattern as `useCreateFlowMdUp`. */
|
||||
const CREATE_FLOW_MIN_WIDTH_LG = "(min-width: 1024px)";
|
||||
|
||||
/** True at viewport ≥1024px (e.g. review grid column split with Tailwind `lg:`). */
|
||||
export function useCreateFlowLgUp(): boolean {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const isLgOrLarger = useMediaQuery(CREATE_FLOW_MIN_WIDTH_LG);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer until mount for SSR/first-paint alignment
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
return !isMounted || isLgOrLarger;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMediaQuery } from "../../../hooks/useMediaQuery";
|
||||
|
||||
/** `--breakpoint-md` (640px); same SSR/first-paint pattern as `useCreateFlowLgUp`. */
|
||||
const CREATE_FLOW_MIN_WIDTH_MD = "(min-width: 640px)";
|
||||
|
||||
/** True at viewport ≥640px (pairs with Tailwind `md:` on create-flow screens). */
|
||||
export function useCreateFlowMdUp(): boolean {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const isMdOrLarger = useMediaQuery(CREATE_FLOW_MIN_WIDTH_MD);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer until mount for SSR/first-paint alignment
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
return !isMounted || isMdOrLarger;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
import type { CreateFlowStep } from "../types";
|
||||
import {
|
||||
type CreateFlowNavigationOptions,
|
||||
getNextStep,
|
||||
getPreviousStep,
|
||||
parseCreateFlowScreenFromPathname,
|
||||
} 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).
|
||||
*/
|
||||
export function useCreateFlowNavigation(
|
||||
options?: CreateFlowNavigationOptions,
|
||||
): {
|
||||
currentStep: CreateFlowStep | null;
|
||||
goToNextStep: () => void;
|
||||
goToPreviousStep: () => void;
|
||||
goToStep: (_step: CreateFlowStep) => void;
|
||||
canGoNext: () => boolean;
|
||||
canGoBack: () => boolean;
|
||||
nextStep: CreateFlowStep | null;
|
||||
previousStep: CreateFlowStep | null;
|
||||
} {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const validStep = parseCreateFlowScreenFromPathname(pathname ?? null);
|
||||
|
||||
const nextStep = getNextStep(validStep, options);
|
||||
const previousStep = getPreviousStep(validStep, options);
|
||||
|
||||
const goToNextStep = useCallback(() => {
|
||||
blurActiveElement();
|
||||
if (nextStep) {
|
||||
router.push(`/create/${nextStep}`);
|
||||
}
|
||||
}, [router, nextStep]);
|
||||
|
||||
const goToPreviousStep = useCallback(() => {
|
||||
blurActiveElement();
|
||||
if (previousStep) {
|
||||
router.push(`/create/${previousStep}`);
|
||||
}
|
||||
}, [router, previousStep]);
|
||||
|
||||
const goToStep = useCallback(
|
||||
(step: CreateFlowStep) => {
|
||||
blurActiveElement();
|
||||
router.push(`/create/${step}`);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
const canGoNext = useCallback(() => nextStep !== null, [nextStep]);
|
||||
const canGoBack = useCallback(() => previousStep !== null, [previousStep]);
|
||||
|
||||
return {
|
||||
currentStep: validStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
goToStep,
|
||||
canGoNext,
|
||||
canGoBack,
|
||||
nextStep,
|
||||
previousStep,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user