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:
adilallo committed 2026-11-05 16:14:04 -07:00
1 parent ca1f58a50c
commit 2348a9e94f
61 files changed
+1110 -421

No files matched your search

+161 -46
View File
@@ -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