Walk Create Community through the Figma screens and restore a chosen logo after reload, so a guest can leave without saving until they reach the email step.
This commit is contained in:
1 parent
ca1f58a50c
commit
2348a9e94f
61 files changed
+1110
-421
No files matched your search
@@ -87,7 +87,9 @@ import {
|
||||
} from "./utils/customRuleConfirmFooterSteps";
|
||||
import {
|
||||
getDefaultFooterLabel,
|
||||
isCommunityNameStepBlocked,
|
||||
isCommunitySizeStepBlocked,
|
||||
isCommunityStructureStepBlocked,
|
||||
} from "./utils/createFlowFooterLabels";
|
||||
import { useAuthModal } from "../../contexts/AuthModalContext";
|
||||
import { useMessages, useTranslation } from "../../contexts/MessagesContext";
|
||||
@@ -102,6 +104,10 @@ import {
|
||||
CreateFlowDraftSaveBannerProvider,
|
||||
useCreateFlowDraftSaveBanner,
|
||||
} from "./context/CreateFlowDraftSaveBannerContext";
|
||||
import {
|
||||
CreateFlowContinueAttemptProvider,
|
||||
useCreateFlowContinueAttempt,
|
||||
} from "./context/CreateFlowContinueAttemptContext";
|
||||
|
||||
function CreateFlowSessionShell({ children }: { children: ReactNode }) {
|
||||
const [sessionUser, setSessionUser] = useState<
|
||||
@@ -128,6 +134,7 @@ function CreateFlowSessionShell({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<CreateFlowProvider enableLocalDraftMirroring={enableLocalDraftMirroring}>
|
||||
<CreateFlowDraftSaveBannerProvider>
|
||||
<CreateFlowContinueAttemptProvider>
|
||||
<Suspense fallback={null}>
|
||||
<CreateFlowLayoutContent
|
||||
sessionUser={sessionUser}
|
||||
@@ -136,6 +143,7 @@ function CreateFlowSessionShell({ children }: { children: ReactNode }) {
|
||||
{children}
|
||||
</CreateFlowLayoutContent>
|
||||
</Suspense>
|
||||
</CreateFlowContinueAttemptProvider>
|
||||
</CreateFlowDraftSaveBannerProvider>
|
||||
</CreateFlowProvider>
|
||||
);
|
||||
@@ -191,13 +199,14 @@ function CreateFlowLayoutContent({
|
||||
editingPublishedRuleIdTrimmed.length > 0;
|
||||
const { draftSaveBannerMessage, setDraftSaveBannerMessage } =
|
||||
useCreateFlowDraftSaveBanner();
|
||||
const { markAttempted, clearAttempted } = useCreateFlowContinueAttempt();
|
||||
const [communitySaveMagicLinkSubmitting, setCommunitySaveMagicLinkSubmitting] =
|
||||
useState(false);
|
||||
const [communitySaveMagicLinkError, setCommunitySaveMagicLinkError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [communitySaveMagicLinkSuccess, setCommunitySaveMagicLinkSuccess] =
|
||||
useState(false);
|
||||
const [saveSentEmail, setSaveSentEmail] = useState<string | null>(null);
|
||||
const [isOffline, setIsOffline] = useState(false);
|
||||
const [completedFlowBanner, setCompletedFlowBanner] = useState<{
|
||||
key: string;
|
||||
status: "positive" | "danger" | "warning";
|
||||
@@ -208,24 +217,29 @@ function CreateFlowLayoutContent({
|
||||
const [leaveConfirmOpen, setLeaveConfirmOpen] = useState(false);
|
||||
const [completedHasPublishedRule, setCompletedHasPublishedRule] =
|
||||
useState(false);
|
||||
const leaveConfirmResolverRef = useRef<((proceed: boolean) => void) | null>(
|
||||
null,
|
||||
);
|
||||
type LeaveConfirmChoice = "leave" | "save" | "cancel";
|
||||
const leaveConfirmResolverRef = useRef<
|
||||
((choice: LeaveConfirmChoice) => void) | null
|
||||
>(null);
|
||||
|
||||
const confirmLeave = useCallback(
|
||||
const requestLeaveChoice = useCallback(
|
||||
() =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
new Promise<LeaveConfirmChoice>((resolve) => {
|
||||
leaveConfirmResolverRef.current = resolve;
|
||||
setLeaveConfirmOpen(true);
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const closeLeaveConfirm = useCallback((proceed: boolean) => {
|
||||
const confirmLeave = useCallback(async () => {
|
||||
return (await requestLeaveChoice()) === "leave";
|
||||
}, [requestLeaveChoice]);
|
||||
|
||||
const closeLeaveConfirm = useCallback((choice: LeaveConfirmChoice) => {
|
||||
setLeaveConfirmOpen(false);
|
||||
const resolve = leaveConfirmResolverRef.current;
|
||||
leaveConfirmResolverRef.current = null;
|
||||
resolve?.(proceed);
|
||||
resolve?.(choice);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
@@ -359,6 +373,25 @@ function CreateFlowLayoutContent({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts?.saveDraft) {
|
||||
const choice = await requestLeaveChoice();
|
||||
if (choice === "cancel") return;
|
||||
if (choice === "save") {
|
||||
if (sessionUser) {
|
||||
await runAuthenticatedExit({ saveDraft: true });
|
||||
return;
|
||||
}
|
||||
router.push(createFlowStepPath("community-save"));
|
||||
return;
|
||||
}
|
||||
if (sessionUser) {
|
||||
await runAuthenticatedExit({ saveDraft: false, skipConfirm: true });
|
||||
return;
|
||||
}
|
||||
router.push(CREATE_ROUTES.root);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionUser === null) {
|
||||
const returnToTemplateReview =
|
||||
templateReviewSlug != null
|
||||
@@ -388,10 +421,25 @@ function CreateFlowLayoutContent({
|
||||
}
|
||||
}, [sessionResolved, sessionUser, currentStep, router]);
|
||||
|
||||
useEffect(() => {
|
||||
clearAttempted();
|
||||
}, [currentStep, clearAttempted]);
|
||||
|
||||
useEffect(() => {
|
||||
const onOffline = () => setIsOffline(true);
|
||||
const onOnline = () => setIsOffline(false);
|
||||
setIsOffline(typeof navigator !== "undefined" && !navigator.onLine);
|
||||
window.addEventListener("offline", onOffline);
|
||||
window.addEventListener("online", onOnline);
|
||||
return () => {
|
||||
window.removeEventListener("offline", onOffline);
|
||||
window.removeEventListener("online", onOnline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentStep !== "community-save") {
|
||||
setCommunitySaveMagicLinkError(null);
|
||||
setCommunitySaveMagicLinkSuccess(false);
|
||||
setCommunitySaveMagicLinkSubmitting(false);
|
||||
}
|
||||
}, [currentStep]);
|
||||
@@ -495,7 +543,6 @@ function CreateFlowLayoutContent({
|
||||
formEl?: HTMLFormElement | null,
|
||||
) => {
|
||||
setCommunitySaveMagicLinkError(null);
|
||||
setCommunitySaveMagicLinkSuccess(false);
|
||||
const namedEmail = formEl?.elements.namedItem("email");
|
||||
const emailField =
|
||||
namedEmail instanceof HTMLInputElement
|
||||
@@ -505,10 +552,6 @@ function CreateFlowLayoutContent({
|
||||
: document.querySelector<HTMLInputElement>(
|
||||
`input[name="email"][form="${CREATE_FLOW_COMMUNITY_SAVE_FORM_ID}"]`,
|
||||
);
|
||||
if (emailField != null && !emailField.checkValidity()) {
|
||||
emailField.reportValidity();
|
||||
return;
|
||||
}
|
||||
const raw =
|
||||
emailField != null ? emailField.value : state.communitySaveEmail;
|
||||
if (typeof raw !== "string" || !isValidCreateFlowSaveEmail(raw)) return;
|
||||
@@ -544,13 +587,30 @@ function CreateFlowLayoutContent({
|
||||
}
|
||||
setTransferPendingFlag();
|
||||
updateState({ communitySaveEmail: trimmed });
|
||||
setCommunitySaveMagicLinkSuccess(true);
|
||||
setSaveSentEmail(trimmed);
|
||||
goToNextStep();
|
||||
} catch {
|
||||
setCommunitySaveMagicLinkError(tLogin("errors.network"));
|
||||
} finally {
|
||||
setCommunitySaveMagicLinkSubmitting(false);
|
||||
}
|
||||
}, [state, currentStep, tLogin, updateState]);
|
||||
}, [state, currentStep, tLogin, updateState, goToNextStep]);
|
||||
|
||||
const handleResendSaveLink = useCallback(async () => {
|
||||
if (!saveSentEmail) return;
|
||||
setCommunitySaveMagicLinkError(null);
|
||||
const nextPath = safeInternalPath("/create/review?syncDraft=1");
|
||||
const draftPayload = buildCreateFlowDraftPayload(state, currentStep);
|
||||
const result = await requestMagicLink(saveSentEmail, nextPath, {
|
||||
...draftPayload,
|
||||
communitySaveEmail: saveSentEmail,
|
||||
});
|
||||
if (result.ok === false) {
|
||||
setCommunitySaveMagicLinkError(
|
||||
result.error || tLogin("errors.generic"),
|
||||
);
|
||||
}
|
||||
}, [saveSentEmail, state, currentStep, tLogin]);
|
||||
|
||||
const isCompletedStep = currentStep === "completed";
|
||||
const isRightRailStep = currentStep === "decision-approaches";
|
||||
@@ -589,6 +649,11 @@ function CreateFlowLayoutContent({
|
||||
const proportionBarProgress = getProportionBarProgressForCreateFlowStep(
|
||||
currentStep,
|
||||
);
|
||||
const hasCommunityLogo =
|
||||
(typeof state.communityAvatarUrl === "string" &&
|
||||
state.communityAvatarUrl.trim().length > 0) ||
|
||||
(typeof state.communityAvatarFileName === "string" &&
|
||||
state.communityAvatarFileName.trim().length > 0);
|
||||
|
||||
/**
|
||||
* Custom Rule stage "confirm selection" steps: all five render the same
|
||||
@@ -617,6 +682,9 @@ function CreateFlowLayoutContent({
|
||||
title: string;
|
||||
description?: string;
|
||||
onClose: () => void;
|
||||
actionLabel?: string;
|
||||
onAction?: () => void;
|
||||
type?: "banner" | "toast";
|
||||
}> = [
|
||||
draftSaveBannerMessage
|
||||
? {
|
||||
@@ -655,13 +723,28 @@ function CreateFlowLayoutContent({
|
||||
onClose: () => setCommunitySaveMagicLinkError(null),
|
||||
}
|
||||
: null,
|
||||
communitySaveMagicLinkSuccess
|
||||
saveSentEmail
|
||||
? {
|
||||
key: "magicLinkSuccess",
|
||||
status: "positive" as const,
|
||||
title: communitySaveMessages.magicLinkSuccessTitle,
|
||||
description: communitySaveMessages.magicLinkSuccessDescription,
|
||||
onClose: () => setCommunitySaveMagicLinkSuccess(false),
|
||||
type: "toast" as const,
|
||||
title: communitySaveMessages.sentToastTitle,
|
||||
description: communitySaveMessages.sentToastDescription.replace(
|
||||
"{email}",
|
||||
saveSentEmail,
|
||||
),
|
||||
actionLabel: communitySaveMessages.sentToastAction,
|
||||
onAction: () => void handleResendSaveLink(),
|
||||
onClose: () => setSaveSentEmail(null),
|
||||
}
|
||||
: null,
|
||||
isOffline
|
||||
? {
|
||||
key: "offline",
|
||||
status: "warning" as const,
|
||||
title: messages.create.topNav.offlineTitle,
|
||||
description: messages.create.topNav.offlineDescription,
|
||||
onClose: () => setIsOffline(false),
|
||||
}
|
||||
: null,
|
||||
completedFlowBanner
|
||||
@@ -688,11 +771,13 @@ function CreateFlowLayoutContent({
|
||||
className="pointer-events-auto mx-auto w-full max-w-[960px]"
|
||||
>
|
||||
<Alert
|
||||
type="banner"
|
||||
type={b.type ?? "banner"}
|
||||
status={b.status}
|
||||
title={b.title}
|
||||
description={b.description}
|
||||
onClose={b.onClose}
|
||||
actionLabel={b.actionLabel}
|
||||
onAction={b.onAction}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -725,22 +810,23 @@ function CreateFlowLayoutContent({
|
||||
/>
|
||||
<Create
|
||||
isOpen={leaveConfirmOpen}
|
||||
onClose={() => closeLeaveConfirm(false)}
|
||||
onClose={() => closeLeaveConfirm("cancel")}
|
||||
title={messages.create.topNav.leaveConfirmTitle}
|
||||
description={messages.create.topNav.leaveConfirmDescription}
|
||||
showBackButton={false}
|
||||
showNextButton
|
||||
nextButtonText={messages.create.topNav.leaveConfirmProceed}
|
||||
onNext={() => closeLeaveConfirm(true)}
|
||||
nextButtonText={messages.create.topNav.leaveConfirmSaveProgress}
|
||||
onNext={() => closeLeaveConfirm("save")}
|
||||
footerContent={
|
||||
<div className={MODAL_FOOTER_START_SLOT_CLASS}>
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
buttonType="outline"
|
||||
danger
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
onClick={() => closeLeaveConfirm(false)}
|
||||
onClick={() => closeLeaveConfirm("leave")}
|
||||
>
|
||||
{messages.create.topNav.leaveConfirmCancel}
|
||||
{messages.create.topNav.leaveConfirmProceed}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
@@ -801,6 +887,7 @@ function CreateFlowLayoutContent({
|
||||
className="hidden"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
markAttempted();
|
||||
void handleCommunitySaveMagicLinkSubmit(e.currentTarget);
|
||||
}}
|
||||
/>
|
||||
@@ -855,13 +942,13 @@ function CreateFlowLayoutContent({
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={
|
||||
isPublishing ||
|
||||
typeof state.title !== "string" ||
|
||||
state.title.trim().length === 0
|
||||
}
|
||||
disabled={isPublishing}
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||
onClick={() => {
|
||||
markAttempted();
|
||||
if (isCommunityNameStepBlocked(currentStep, state.title)) {
|
||||
return;
|
||||
}
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
@@ -879,7 +966,7 @@ function CreateFlowLayoutContent({
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{footer.saveLater}
|
||||
{footer.skipForNow}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -888,10 +975,7 @@ function CreateFlowLayoutContent({
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={
|
||||
isPublishing ||
|
||||
communitySaveMagicLinkSubmitting ||
|
||||
communitySaveMagicLinkSuccess ||
|
||||
!isValidCreateFlowSaveEmail(state.communitySaveEmail)
|
||||
isPublishing || communitySaveMagicLinkSubmitting
|
||||
}
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||
>
|
||||
@@ -900,6 +984,19 @@ function CreateFlowLayoutContent({
|
||||
: footer.submitEmail}
|
||||
</Button>
|
||||
</div>
|
||||
) : currentStep === "community-upload" && nextStep ? (
|
||||
<Button
|
||||
buttonType={hasCommunityLogo ? "filled" : "outline"}
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={isPublishing}
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||
onClick={() => {
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{hasCommunityLogo ? footer.next : footer.skip}
|
||||
</Button>
|
||||
) : currentStep === "review" && nextStep ? (
|
||||
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||
<Button
|
||||
@@ -988,20 +1085,38 @@ function CreateFlowLayoutContent({
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={
|
||||
isPublishing ||
|
||||
(isFinalReviewLike && !sessionResolved) ||
|
||||
isCommunitySizeStepBlocked(
|
||||
currentStep,
|
||||
state.selectedCommunitySizeIds,
|
||||
)
|
||||
isPublishing || (isFinalReviewLike && !sessionResolved)
|
||||
}
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||
onClick={() => {
|
||||
if (isFinalReviewLike) {
|
||||
void handleFinalize();
|
||||
} else {
|
||||
goToNextStep();
|
||||
return;
|
||||
}
|
||||
markAttempted();
|
||||
if (
|
||||
isCommunityStructureStepBlocked(
|
||||
currentStep,
|
||||
state.selectedOrganizationTypeIds,
|
||||
state.selectedMaturityIds,
|
||||
) ||
|
||||
isCommunitySizeStepBlocked(
|
||||
currentStep,
|
||||
state.selectedCommunitySizeIds,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (reviewReturnTarget) {
|
||||
router.push(
|
||||
createFlowStepPathAfterStrippingReviewReturn(
|
||||
reviewReturnTarget,
|
||||
searchParams,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{isFinalReviewLike
|
||||
|
||||
Reference in new issue
Block a user