Create Community stage implemented
This commit is contained in:
@@ -12,12 +12,20 @@ import { CreateFlowProvider, useCreateFlow } from "./context/CreateFlowContext";
|
||||
import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation";
|
||||
import { useCreateFlowExit } from "./hooks/useCreateFlowExit";
|
||||
import CreateFlowTopNav from "../components/utility/CreateFlowTopNav";
|
||||
import { getStepIndex } from "./utils/flowSteps";
|
||||
import { getNextStep, getStepIndex } from "./utils/flowSteps";
|
||||
import { getProportionBarProgressForCreateFlowStep } from "./utils/createFlowProportionProgress";
|
||||
import { createFlowStepUsesCenteredTextLayout } from "./utils/createFlowScreenRegistry";
|
||||
import CreateFlowFooter from "../components/utility/CreateFlowFooter";
|
||||
import Button from "../components/buttons/Button";
|
||||
import { buildPublishPayload } from "../../lib/create/buildPublishPayload";
|
||||
import { fetchAuthSession, publishRule } from "../../lib/create/api";
|
||||
import { isValidCreateFlowSaveEmail } from "../../lib/create/isValidCreateFlowSaveEmail";
|
||||
import {
|
||||
fetchAuthSession,
|
||||
publishRule,
|
||||
requestMagicLink,
|
||||
} from "../../lib/create/api";
|
||||
import { safeInternalPath } from "../../lib/safeInternalPath";
|
||||
import { setTransferPendingFlag } from "./utils/anonymousDraftStorage";
|
||||
import { writeLastPublishedRule } from "../../lib/create/lastPublishedRule";
|
||||
import {
|
||||
fetchTemplateBySlug,
|
||||
@@ -25,7 +33,7 @@ import {
|
||||
} from "../../lib/create/fetchTemplates";
|
||||
import messages from "../../messages/en/index";
|
||||
import { useAuthModal } from "../contexts/AuthModalContext";
|
||||
import { useTranslation } from "../contexts/MessagesContext";
|
||||
import { useMessages, useTranslation } from "../contexts/MessagesContext";
|
||||
import { PostLoginDraftTransfer } from "./PostLoginDraftTransfer";
|
||||
import { SignedInDraftHydration } from "./SignedInDraftHydration";
|
||||
import Alert from "../components/modals/Alert";
|
||||
@@ -35,7 +43,7 @@ import {
|
||||
} from "./context/CreateFlowDraftSaveBannerContext";
|
||||
|
||||
/** First step where Save & Exit is offered (first Create Community select per Figma). */
|
||||
const SAVE_EXIT_FROM_STEP_INDEX = getStepIndex("community-size");
|
||||
const SAVE_EXIT_FROM_STEP_INDEX = getStepIndex("community-structure");
|
||||
|
||||
function CreateFlowSessionShell({ children }: { children: ReactNode }) {
|
||||
const [sessionUser, setSessionUser] = useState<
|
||||
@@ -78,7 +86,10 @@ function CreateFlowLayoutContent({
|
||||
sessionUser: { id: string; email: string } | null | undefined;
|
||||
sessionResolved: boolean;
|
||||
}) {
|
||||
const tFooter = useTranslation("create.footer");
|
||||
const { create } = useMessages();
|
||||
const footer = create.footer;
|
||||
const communitySaveMessages = create.communitySave;
|
||||
const tLogin = useTranslation("pages.login");
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { openLogin } = useAuthModal();
|
||||
@@ -89,7 +100,7 @@ function CreateFlowLayoutContent({
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
} = useCreateFlowNavigation();
|
||||
const { state, clearState } = useCreateFlow();
|
||||
const { state, clearState, updateState } = useCreateFlow();
|
||||
const { draftSaveBannerMessage, setDraftSaveBannerMessage } =
|
||||
useCreateFlowDraftSaveBanner();
|
||||
const [publishBannerMessage, setPublishBannerMessage] = useState<
|
||||
@@ -100,6 +111,13 @@ function CreateFlowLayoutContent({
|
||||
string | null
|
||||
>(null);
|
||||
const [isApplyingTemplate, setIsApplyingTemplate] = useState(false);
|
||||
const [communitySaveMagicLinkSubmitting, setCommunitySaveMagicLinkSubmitting] =
|
||||
useState(false);
|
||||
const [communitySaveMagicLinkError, setCommunitySaveMagicLinkError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [communitySaveMagicLinkSuccess, setCommunitySaveMagicLinkSuccess] =
|
||||
useState(false);
|
||||
|
||||
const templateReviewMatch = pathname?.match(
|
||||
/\/create\/review-template\/([^/?#]+)/,
|
||||
@@ -222,6 +240,51 @@ function CreateFlowLayoutContent({
|
||||
await runAuthenticatedExit(opts);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentStep !== "community-save") {
|
||||
setCommunitySaveMagicLinkError(null);
|
||||
setCommunitySaveMagicLinkSuccess(false);
|
||||
setCommunitySaveMagicLinkSubmitting(false);
|
||||
}
|
||||
}, [currentStep]);
|
||||
|
||||
const handleCommunitySaveMagicLinkSubmit = useCallback(async () => {
|
||||
setCommunitySaveMagicLinkError(null);
|
||||
setCommunitySaveMagicLinkSuccess(false);
|
||||
const raw = state.communitySaveEmail;
|
||||
const trimmed = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
||||
if (!isValidCreateFlowSaveEmail(trimmed)) return;
|
||||
|
||||
setCommunitySaveMagicLinkSubmitting(true);
|
||||
try {
|
||||
const stepAfterSave = getNextStep("community-save");
|
||||
const segment = stepAfterSave ?? "review";
|
||||
const rawNext = `/create/${segment}?syncDraft=1`;
|
||||
const nextPath = safeInternalPath(rawNext);
|
||||
const result = await requestMagicLink(trimmed, nextPath);
|
||||
if (result.ok === false) {
|
||||
if (result.retryAfterMs != null && result.retryAfterMs > 0) {
|
||||
const seconds = Math.ceil(result.retryAfterMs / 1000);
|
||||
setCommunitySaveMagicLinkError(
|
||||
tLogin("errors.rateLimited").replace("{seconds}", String(seconds)),
|
||||
);
|
||||
} else {
|
||||
setCommunitySaveMagicLinkError(
|
||||
result.error || tLogin("errors.generic"),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setTransferPendingFlag();
|
||||
updateState({ communitySaveEmail: trimmed });
|
||||
setCommunitySaveMagicLinkSuccess(true);
|
||||
} catch {
|
||||
setCommunitySaveMagicLinkError(tLogin("errors.network"));
|
||||
} finally {
|
||||
setCommunitySaveMagicLinkSubmitting(false);
|
||||
}
|
||||
}, [state.communitySaveEmail, tLogin, updateState]);
|
||||
|
||||
const isCompletedStep = currentStep === "completed";
|
||||
const isRightRailStep = currentStep === "right-rail";
|
||||
const isFinalReviewStep = currentStep === "final-review";
|
||||
@@ -250,14 +313,23 @@ function CreateFlowLayoutContent({
|
||||
const saveDraftOnExit =
|
||||
Boolean(sessionUser) && stepIdx >= SAVE_EXIT_FROM_STEP_INDEX;
|
||||
|
||||
const hasErrorOverlays =
|
||||
const proportionBarProgress = getProportionBarProgressForCreateFlowStep(
|
||||
currentStep,
|
||||
);
|
||||
|
||||
const footerPrimaryButtonClass =
|
||||
"md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]";
|
||||
|
||||
const hasTopOverlays =
|
||||
Boolean(draftSaveBannerMessage) ||
|
||||
Boolean(publishBannerMessage) ||
|
||||
Boolean(templateReviewApplyError);
|
||||
Boolean(templateReviewApplyError) ||
|
||||
Boolean(communitySaveMagicLinkError) ||
|
||||
Boolean(communitySaveMagicLinkSuccess);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-screen min-h-0 flex-col overflow-hidden bg-black">
|
||||
{hasErrorOverlays ? (
|
||||
{hasTopOverlays ? (
|
||||
<div
|
||||
className="pointer-events-none fixed left-0 right-0 top-0 z-[200] flex flex-col gap-2 px-[var(--spacing-measures-spacing-500,20px)] pt-[var(--spacing-measures-spacing-300,12px)] md:px-[var(--measures-spacing-1800,64px)]"
|
||||
aria-live="polite"
|
||||
@@ -298,6 +370,30 @@ function CreateFlowLayoutContent({
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{communitySaveMagicLinkError ? (
|
||||
<div className="pointer-events-auto mx-auto w-full max-w-[960px]">
|
||||
<Alert
|
||||
type="banner"
|
||||
status="danger"
|
||||
title={communitySaveMessages.magicLinkErrorTitle}
|
||||
description={communitySaveMagicLinkError}
|
||||
onClose={() => setCommunitySaveMagicLinkError(null)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{communitySaveMagicLinkSuccess ? (
|
||||
<div className="pointer-events-auto mx-auto w-full max-w-[960px]">
|
||||
<Alert
|
||||
type="banner"
|
||||
status="positive"
|
||||
title={communitySaveMessages.magicLinkSuccessTitle}
|
||||
description={communitySaveMessages.magicLinkSuccessDescription}
|
||||
onClose={() => setCommunitySaveMagicLinkSuccess(false)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<Suspense fallback={null}>
|
||||
@@ -334,6 +430,8 @@ function CreateFlowLayoutContent({
|
||||
<CreateFlowFooter
|
||||
className="shrink-0"
|
||||
progressBar={!isTemplateReviewRoute && !isFinalReviewStep}
|
||||
proportionBarProgress={proportionBarProgress}
|
||||
proportionBarVariant="segmented"
|
||||
secondButton={
|
||||
isTemplateReviewRoute ? (
|
||||
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||
@@ -367,13 +465,101 @@ function CreateFlowLayoutContent({
|
||||
{messages.create.templateReview.footer.customize}
|
||||
</Button>
|
||||
</div>
|
||||
) : currentStep === "community-name" && nextStep ? (
|
||||
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="inverse"
|
||||
size="xsmall"
|
||||
disabled={isPublishing}
|
||||
className={footerPrimaryButtonClass}
|
||||
onClick={() => {
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{footer.next}
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={isPublishing}
|
||||
className={footerPrimaryButtonClass}
|
||||
onClick={() => {
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{footer.confirmName}
|
||||
</Button>
|
||||
</div>
|
||||
) : currentStep === "community-save" && nextStep ? (
|
||||
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={isPublishing}
|
||||
className={footerPrimaryButtonClass}
|
||||
onClick={() => {
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{footer.saveLater}
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={
|
||||
isPublishing ||
|
||||
communitySaveMagicLinkSubmitting ||
|
||||
communitySaveMagicLinkSuccess ||
|
||||
!isValidCreateFlowSaveEmail(state.communitySaveEmail)
|
||||
}
|
||||
className={footerPrimaryButtonClass}
|
||||
onClick={() => {
|
||||
void handleCommunitySaveMagicLinkSubmit();
|
||||
}}
|
||||
>
|
||||
{communitySaveMagicLinkSubmitting
|
||||
? footer.submitEmailSending
|
||||
: footer.submitEmail}
|
||||
</Button>
|
||||
</div>
|
||||
) : currentStep === "review" && nextStep ? (
|
||||
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={isPublishing}
|
||||
className={footerPrimaryButtonClass}
|
||||
onClick={() => {
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{footer.createCustom}
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={isPublishing}
|
||||
className={footerPrimaryButtonClass}
|
||||
onClick={() => {
|
||||
router.push("/templates");
|
||||
}}
|
||||
>
|
||||
{footer.createFromTemplate}
|
||||
</Button>
|
||||
</div>
|
||||
) : nextStep ? (
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={isPublishing}
|
||||
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
|
||||
className={footerPrimaryButtonClass}
|
||||
onClick={() => {
|
||||
if (currentStep === "final-review") {
|
||||
void handleFinalize();
|
||||
@@ -385,10 +571,16 @@ function CreateFlowLayoutContent({
|
||||
{currentStep === "final-review"
|
||||
? isPublishing
|
||||
? messages.create.publish.finalizeButtonPublishing
|
||||
: tFooter("finalizeCommunityRule")
|
||||
: footer.finalizeCommunityRule
|
||||
: currentStep === "confirm-stakeholders"
|
||||
? tFooter("confirmStakeholders")
|
||||
: tFooter("next")}
|
||||
? footer.confirmStakeholders
|
||||
: currentStep === "community-context"
|
||||
? footer.confirmDescription
|
||||
: currentStep === "community-structure"
|
||||
? footer.confirmDetails
|
||||
: currentStep === "community-size"
|
||||
? footer.confirmMembers
|
||||
: footer.next}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { notFound } from "next/navigation";
|
||||
import { use } from "react";
|
||||
import { notFound, useRouter } from "next/navigation";
|
||||
import { use, useEffect } from "react";
|
||||
import { CreateFlowScreenView } from "../screens/CreateFlowScreenView";
|
||||
import { isValidStep } from "../utils/flowSteps";
|
||||
import type { CreateFlowStep } from "../types";
|
||||
@@ -12,6 +12,17 @@ interface PageProps {
|
||||
|
||||
export default function CreateFlowScreenPage({ params }: PageProps) {
|
||||
const { screenId: raw } = use(params);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (raw === "community-reflection") {
|
||||
router.replace("/create/community-save");
|
||||
}
|
||||
}, [raw, router]);
|
||||
|
||||
if (raw === "community-reflection") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isValidStep(raw)) {
|
||||
notFound();
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { CreateFlowHeaderLockup } from "./CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "./CreateFlowStepShell";
|
||||
import {
|
||||
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
||||
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||
} from "./createFlowLayoutTokens";
|
||||
|
||||
/** Shared `RuleCard` / template card chrome: width + radius; padding comes from `RuleCard` (L+expanded = 24px). */
|
||||
export const CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS =
|
||||
"w-full min-w-0 rounded-[12px] md:rounded-[24px] md:!max-w-full md:!w-full";
|
||||
"w-full min-w-0 rounded-[12px] md:rounded-[24px] md:max-w-[640px]";
|
||||
|
||||
type CreateFlowLockupCardStepShellProps = {
|
||||
lockupTitle: string;
|
||||
@@ -14,10 +18,7 @@ type CreateFlowLockupCardStepShellProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Final-review-style create-flow step: `wideGrid` shell, two-column grid at `md+`,
|
||||
* left `CreateFlowHeaderLockup` (vertically centered in column), right column for card content.
|
||||
*/
|
||||
/** Final-review layout: `wideGrid`, two columns from `md:`, column widths from `createFlowLayoutTokens`. */
|
||||
export function CreateFlowLockupCardStepShell({
|
||||
lockupTitle,
|
||||
lockupDescription,
|
||||
@@ -25,15 +26,23 @@ export function CreateFlowLockupCardStepShell({
|
||||
}: CreateFlowLockupCardStepShellProps) {
|
||||
return (
|
||||
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
||||
<div className="flex w-full min-w-0 flex-col gap-4 md:grid md:grid-cols-2 md:gap-[var(--measures-spacing-1200,48px)]">
|
||||
<div className="flex min-w-0 flex-col justify-start md:justify-center">
|
||||
<div
|
||||
className={`mx-auto flex w-full min-w-0 flex-col gap-4 md:grid md:w-full md:grid-cols-2 md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
||||
>
|
||||
<div
|
||||
className={`flex min-w-0 flex-col justify-start md:justify-center ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||
>
|
||||
<CreateFlowHeaderLockup
|
||||
title={lockupTitle}
|
||||
description={lockupDescription}
|
||||
justification="left"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-0 w-full flex-col items-stretch">{children}</div>
|
||||
<div
|
||||
className={`flex min-w-0 flex-col items-stretch ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</CreateFlowStepShell>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ export type CreateFlowStepShellVariant =
|
||||
| "wideGridLoosePadding"
|
||||
| "bare";
|
||||
|
||||
/** Top padding below `md` between top nav and step content (semantic space tokens). */
|
||||
/** Semantic top padding below create-flow top nav (applied at all breakpoints; name is legacy). */
|
||||
export type CreateFlowContentTopBelowMd = "none" | "space-1400" | "space-800";
|
||||
|
||||
const outerByVariant: Record<CreateFlowStepShellVariant, string> = {
|
||||
@@ -17,22 +17,24 @@ const outerByVariant: Record<CreateFlowStepShellVariant, string> = {
|
||||
"flex w-full min-w-0 flex-col items-center px-5 md:px-16",
|
||||
centeredNarrowBottomPad:
|
||||
"flex w-full min-w-0 flex-col items-center px-5 pb-28 md:px-[var(--measures-spacing-1800,64px)] md:pb-32",
|
||||
wideGrid: "w-full min-w-0 max-w-[1280px] shrink-0 px-5 md:px-12",
|
||||
/** Wide two-column steps; 1328px = two 640px columns + 48px gutter. */
|
||||
wideGrid: "w-full min-w-0 max-w-[1328px] shrink-0 px-5 md:px-12",
|
||||
/** Create Community review + card grid (Figma Flow — Review `19706:12135`): max width 1440. */
|
||||
wideGridLoosePadding:
|
||||
"w-full min-w-0 max-w-[1280px] shrink-0 px-5 md:px-16",
|
||||
"w-full min-w-0 max-w-[1440px] shrink-0 px-5 md:px-16",
|
||||
bare: "w-full min-w-0",
|
||||
};
|
||||
|
||||
const contentTopBelowMdClass: Record<CreateFlowContentTopBelowMd, string> = {
|
||||
none: "",
|
||||
"space-1400": "max-md:pt-[var(--space-1400)]",
|
||||
"space-800": "max-md:pt-[var(--space-800)]",
|
||||
"space-1400": "pt-[var(--space-1400)]",
|
||||
"space-800": "pt-[var(--space-800)]",
|
||||
};
|
||||
|
||||
interface CreateFlowStepShellProps {
|
||||
children: ReactNode;
|
||||
variant?: CreateFlowStepShellVariant;
|
||||
/** Padding-top below `md` only; `text` step uses `none`. */
|
||||
/** Top spacing below top chrome (`CreateFlowTextFieldScreen` defaults to `space-1400`). */
|
||||
contentTopBelowMd?: CreateFlowContentTopBelowMd;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/** Single column/section: full width under `md`, max 640px from `--breakpoint-md` up. */
|
||||
export const CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS =
|
||||
"w-full min-w-0 md:max-w-[640px]";
|
||||
|
||||
/** Grid cell: same cap as column max, centered when the track is wider than 640px. */
|
||||
export const CREATE_FLOW_MD_UP_GRID_CELL_CLASS =
|
||||
"w-full min-w-0 md:mx-auto md:max-w-[640px]";
|
||||
|
||||
/** Two 640px columns + `--measures-spacing-1200` (48px) gutter. */
|
||||
export const CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS = "md:max-w-[1328px]";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,19 +3,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMediaQuery } from "../../hooks/useMediaQuery";
|
||||
|
||||
/**
|
||||
* Matches design-system `md` (`--breakpoint-md`, 640px in `app/tailwind.css`).
|
||||
* Use with Tailwind `md:` / `max-md:` utilities in create-flow pages.
|
||||
*/
|
||||
/** `--breakpoint-md` (640px); same SSR/first-paint pattern as `useCreateFlowLgUp`. */
|
||||
const CREATE_FLOW_MIN_WIDTH_MD = "(min-width: 640px)";
|
||||
|
||||
/**
|
||||
* True at or above the create-flow `md` breakpoint (desktop-oriented layout).
|
||||
*
|
||||
* `useMediaQuery` initializes to `false` on the server and first client render
|
||||
* to avoid hydration mismatches. We combine it with a post-mount flag so the
|
||||
* first paint matches the intended desktop layout until `matchMedia` runs.
|
||||
*/
|
||||
/** 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);
|
||||
|
||||
@@ -15,16 +15,14 @@ import {
|
||||
CreateFlowLockupCardStepShell,
|
||||
} from "../../components/CreateFlowLockupCardStepShell";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template review: same responsive grid and RuleCard chrome as final-review;
|
||||
* copy from Figma 22142-898702 (intro + dynamic card from API).
|
||||
*/
|
||||
/** Template review route — same shell/grid as final-review; Figma `22142-898702`. */
|
||||
export default function ReviewTemplatePage({ params }: PageProps) {
|
||||
const { slug: rawSlug } = use(params);
|
||||
const slug = decodeURIComponent(rawSlug);
|
||||
@@ -75,7 +73,9 @@ export default function ReviewTemplatePage({ params }: PageProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
||||
<div className="flex w-full shrink-0 items-center justify-start pb-16">
|
||||
<div
|
||||
className={`flex shrink-0 items-center justify-start pb-16 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||
>
|
||||
<p className="text-[var(--color-content-default-secondary,#a3a3a3)]">
|
||||
{t("loading")}
|
||||
</p>
|
||||
@@ -87,7 +87,9 @@ export default function ReviewTemplatePage({ params }: PageProps) {
|
||||
if (error || !template) {
|
||||
return (
|
||||
<CreateFlowStepShell variant="wideGrid" contentTopBelowMd="space-800">
|
||||
<div className="flex w-full max-w-[640px] shrink-0 flex-col gap-4 pb-8">
|
||||
<div
|
||||
className={`flex shrink-0 flex-col gap-4 pb-8 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||
>
|
||||
<Alert
|
||||
type="banner"
|
||||
status="danger"
|
||||
|
||||
@@ -33,26 +33,31 @@ export function CreateFlowScreenView({
|
||||
maxLength={48}
|
||||
/>
|
||||
);
|
||||
case "community-size":
|
||||
return <CommunitySizeSelectScreen />;
|
||||
case "community-structure":
|
||||
return <CommunityStructureSelectScreen />;
|
||||
case "community-context":
|
||||
return (
|
||||
<CreateFlowTextFieldScreen
|
||||
messageNamespace="create.communityContext"
|
||||
stateField="communityContext"
|
||||
maxLength={2000}
|
||||
maxLength={48}
|
||||
mainAlign="center"
|
||||
/>
|
||||
);
|
||||
case "community-structure":
|
||||
return <CommunityStructureSelectScreen />;
|
||||
case "community-size":
|
||||
return <CommunitySizeSelectScreen />;
|
||||
case "community-upload":
|
||||
return <CommunityUploadScreen />;
|
||||
case "community-reflection":
|
||||
case "community-save":
|
||||
return (
|
||||
<CreateFlowTextFieldScreen
|
||||
messageNamespace="create.communityReflection"
|
||||
stateField="communityReflection"
|
||||
maxLength={2000}
|
||||
messageNamespace="create.communitySave"
|
||||
stateField="communitySaveEmail"
|
||||
maxLength={254}
|
||||
mainAlign="center"
|
||||
inputType="email"
|
||||
showCharacterCount={false}
|
||||
headerJustification="center"
|
||||
/>
|
||||
);
|
||||
case "review":
|
||||
|
||||
@@ -9,6 +9,7 @@ import CardStack from "../../../components/utility/CardStack";
|
||||
import Create from "../../../components/modals/Create";
|
||||
import TextArea from "../../../components/controls/TextArea";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||
|
||||
const IN_PERSON_CARD_ID = "in-person-meetings";
|
||||
const SIGNAL_CARD_ID = "signal";
|
||||
@@ -210,15 +211,15 @@ export function CardsScreen() {
|
||||
variant="wideGridLoosePadding"
|
||||
contentTopBelowMd="space-800"
|
||||
>
|
||||
<div className="flex w-full min-w-0 flex-col gap-6">
|
||||
<div className="min-w-0">
|
||||
<div className="flex w-full min-w-0 flex-col items-center gap-6">
|
||||
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
|
||||
<CreateFlowHeaderLockup
|
||||
title={title}
|
||||
description={description}
|
||||
justification="center"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 w-full">
|
||||
<div className={CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}>
|
||||
<CardStack
|
||||
cards={sampleCards}
|
||||
selectedIds={selectedIds}
|
||||
|
||||
@@ -9,6 +9,10 @@ import { parseDocumentSectionsForDisplay } from "../../../../lib/create/buildPub
|
||||
import { readLastPublishedRule } from "../../../../lib/create/lastPublishedRule";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import {
|
||||
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
||||
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||
} from "../../components/createFlowLayoutTokens";
|
||||
|
||||
export function CompletedScreen() {
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
@@ -67,8 +71,12 @@ export function CompletedScreen() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-0 w-full flex-1 flex-col overflow-hidden bg-[var(--color-teal-teal50,#c9fef9)] md:h-full">
|
||||
<div className="mx-auto grid min-h-0 w-full max-w-[1280px] grid-cols-1 gap-4 px-5 max-md:max-w-[639px] max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:gap-[var(--measures-spacing-1200,48px)] md:overflow-hidden md:px-12 md:py-0">
|
||||
<div className="flex min-w-0 flex-col justify-start overflow-hidden md:justify-center md:pb-8">
|
||||
<div
|
||||
className={`mx-auto grid min-h-0 w-full grid-cols-1 gap-4 px-5 max-md:max-w-[639px] max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] md:overflow-hidden md:px-12 md:py-0 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col justify-start overflow-hidden md:justify-center md:pb-8 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||
>
|
||||
<CreateFlowHeaderLockup
|
||||
title={headerTitle}
|
||||
description={headerDescription}
|
||||
@@ -77,12 +85,14 @@ export function CompletedScreen() {
|
||||
palette="inverse"
|
||||
/>
|
||||
</div>
|
||||
<div className="scrollbar-hide relative flex min-h-0 min-w-0 flex-col overflow-x-hidden md:overflow-y-auto">
|
||||
<div
|
||||
className={`scrollbar-hide relative flex min-h-0 flex-col overflow-x-hidden md:overflow-y-auto ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none sticky top-0 z-10 hidden h-5 shrink-0 bg-gradient-to-b from-[var(--color-teal-teal50,#c9fef9)]/55 from-0% via-[var(--color-teal-teal50,#c9fef9)]/20 via-50% to-transparent md:block"
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="min-w-0 py-0 md:pb-8">
|
||||
<div className="w-full min-w-0 py-0 md:pb-8">
|
||||
<CommunityRuleDocument
|
||||
sections={documentSections}
|
||||
useCardStyle={!mdUp}
|
||||
|
||||
@@ -1,40 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import NumberedList from "../../../components/type/NumberedList";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||
|
||||
/** Create Community — frame 1 (Figma 20094-16005). */
|
||||
/**
|
||||
* Create Community — frame 1 (Figma [20094-16005](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=20094-16005)).
|
||||
* URL: /create/informational
|
||||
*/
|
||||
export function InformationalScreen() {
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.informational");
|
||||
const copy = useMessages().create.informational;
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: t("steps.0.title"),
|
||||
description: t("steps.0.description"),
|
||||
title: copy.steps["0"].title,
|
||||
description: copy.steps["0"].description,
|
||||
},
|
||||
{
|
||||
title: t("steps.1.title"),
|
||||
description: t("steps.1.description"),
|
||||
title: copy.steps["1"].title,
|
||||
description: copy.steps["1"].description,
|
||||
},
|
||||
{
|
||||
title: t("steps.2.title"),
|
||||
description: t("steps.2.description"),
|
||||
title: copy.steps["2"].title,
|
||||
description: copy.steps["2"].description,
|
||||
},
|
||||
];
|
||||
|
||||
const description: ReactNode = (
|
||||
<>
|
||||
{copy.descriptionLead}{" "}
|
||||
<a
|
||||
href="#"
|
||||
className="font-inter font-normal text-[var(--color-content-default-tertiary,#b4b4b4)] underline decoration-solid underline-offset-[3px] cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{copy.workshopLabel}
|
||||
</a>{" "}
|
||||
{copy.descriptionTrail}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<CreateFlowStepShell
|
||||
variant="centeredNarrow"
|
||||
contentTopBelowMd="space-1400"
|
||||
>
|
||||
<div className="flex w-full max-w-[640px] flex-col items-center gap-12">
|
||||
<div
|
||||
className={`flex flex-col items-center gap-[var(--measures-spacing-1200,48px)] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||
>
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("title")}
|
||||
description={t("description")}
|
||||
title={copy.title}
|
||||
description={description}
|
||||
justification="left"
|
||||
/>
|
||||
<NumberedList items={items} size={mdUp ? "M" : "S"} />
|
||||
|
||||
@@ -3,37 +3,56 @@
|
||||
import RuleCard from "../../../components/cards/RuleCard";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowLgUp } from "../../hooks/useCreateFlowLgUp";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import {
|
||||
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
||||
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||
} from "../../components/createFlowLayoutTokens";
|
||||
|
||||
/** Create Community — frame 8 (Figma 19706-12135); URL segment `review`. */
|
||||
/** Create Community review — Figma `19706:12135` (`/create/review`; two columns from `lg:`; column caps in `createFlowLayoutTokens`). */
|
||||
export function CommunityReviewScreen() {
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const lgUp = useCreateFlowLgUp();
|
||||
const t = useTranslation("create.review");
|
||||
const { state } = useCreateFlow();
|
||||
|
||||
const cardTitle =
|
||||
typeof state.title === "string" && state.title.trim().length > 0
|
||||
? state.title.trim()
|
||||
: t("ruleCard.title");
|
||||
const cardDescription =
|
||||
typeof state.communityContext === "string" &&
|
||||
state.communityContext.trim().length > 0
|
||||
? state.communityContext.trim()
|
||||
: t("ruleCard.description");
|
||||
|
||||
return (
|
||||
<CreateFlowStepShell
|
||||
variant="wideGridLoosePadding"
|
||||
contentTopBelowMd="space-1400"
|
||||
>
|
||||
<div className="flex w-full min-w-0 flex-col gap-4 md:grid md:grid-cols-2 md:gap-[var(--measures-spacing-1200,48px)]">
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className={`flex w-full min-w-0 flex-col items-center gap-6 lg:mx-auto lg:w-full lg:grid lg:grid-cols-2 lg:items-center lg:justify-items-center lg:gap-x-[var(--measures-spacing-1200,48px)] lg:gap-y-6 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col justify-center lg:min-h-[212px] ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||
>
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
justification="left"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 w-full">
|
||||
<div className={CREATE_FLOW_MD_UP_GRID_CELL_CLASS}>
|
||||
<RuleCard
|
||||
title={t("ruleCard.title")}
|
||||
description={t("ruleCard.description")}
|
||||
size={mdUp ? "L" : "M"}
|
||||
title={cardTitle}
|
||||
description={cardDescription}
|
||||
size={lgUp ? "L" : "M"}
|
||||
expanded={false}
|
||||
backgroundColor="bg-[#c9fef9]"
|
||||
backgroundColor="bg-[var(--color-teal-teal50,#c9fef9)]"
|
||||
logoUrl="/assets/Vector_MutualAid.svg"
|
||||
logoAlt={t("ruleCard.logoAlt")}
|
||||
className="rounded-[16px]"
|
||||
logoAlt={cardTitle}
|
||||
className="rounded-[24px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,10 @@ import type { CardStackItem } from "../../../components/utility/CardStack/CardSt
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import {
|
||||
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
||||
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||
} from "../../components/createFlowLayoutTokens";
|
||||
|
||||
export function RightRailScreen() {
|
||||
const m = useMessages();
|
||||
@@ -76,8 +80,12 @@ export function RightRailScreen() {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden md:h-full">
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden px-5 max-md:overflow-y-auto md:px-12">
|
||||
<div className="mx-auto grid h-auto min-h-0 w-full max-w-[1280px] shrink-0 grid-cols-1 gap-6 min-w-0 max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:gap-12 md:pb-8">
|
||||
<div className="flex min-w-0 flex-col items-stretch justify-start overflow-hidden md:justify-center">
|
||||
<div
|
||||
className={`mx-auto grid h-auto min-h-0 w-full shrink-0 grid-cols-1 gap-6 min-w-0 max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:justify-items-center md:gap-12 md:pb-8 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col items-stretch justify-start overflow-hidden md:justify-center ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||
>
|
||||
<DecisionMakingSidebar
|
||||
title={rr.sidebar.title}
|
||||
description={sidebarDescription}
|
||||
@@ -89,8 +97,10 @@ export function RightRailScreen() {
|
||||
justification={mdUp ? "left" : "center"}
|
||||
/>
|
||||
</div>
|
||||
<div className="scrollbar-hide relative flex min-h-0 min-w-0 flex-col overflow-x-hidden max-md:overflow-visible md:overflow-y-auto">
|
||||
<div className="flex min-w-0 flex-col items-center gap-6 py-0 md:pb-8">
|
||||
<div
|
||||
className={`scrollbar-hide relative flex min-h-0 flex-col overflow-x-hidden max-md:overflow-visible md:overflow-y-auto ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||
>
|
||||
<div className="flex w-full min-w-0 flex-col items-center gap-6 py-0 md:pb-8">
|
||||
<CardStack
|
||||
cards={sampleCards}
|
||||
selectedIds={selectedIds}
|
||||
|
||||
@@ -1,44 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useEffect, type Dispatch, type SetStateAction } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import MultiSelect from "../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
|
||||
function createListCustomHandlers(
|
||||
setList: Dispatch<SetStateAction<ChipOption[]>>,
|
||||
confirmState: "Unselected" | "Selected",
|
||||
onInteraction?: () => void,
|
||||
) {
|
||||
const touch = () => onInteraction?.();
|
||||
return {
|
||||
onAddClick: () => {
|
||||
touch();
|
||||
setList((prev) => [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), label: "", state: "Custom" },
|
||||
]);
|
||||
},
|
||||
onCustomChipConfirm: (chipId: string, value: string) => {
|
||||
touch();
|
||||
setList((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: confirmState }
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
},
|
||||
onCustomChipClose: (chipId: string) => {
|
||||
touch();
|
||||
setList((prev) => prev.filter((o) => o.id !== chipId));
|
||||
},
|
||||
};
|
||||
}
|
||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||
|
||||
function chipRowsFromLabels(
|
||||
rows: readonly { label: string }[],
|
||||
@@ -56,17 +25,16 @@ function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
||||
.map((o) => o.id);
|
||||
}
|
||||
|
||||
/** Create Community — frame 3 (Figma 20094-18244). */
|
||||
/** Create Community — Figma `20094:41317`, chips only (layout tokens shared with structure select). */
|
||||
export function CommunitySizeSelectScreen() {
|
||||
const m = useMessages();
|
||||
const cs = m.create.communitySize;
|
||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.communitySize");
|
||||
|
||||
const [communitySizeOptions, setCommunitySizeOptions] = useState<
|
||||
ChipOption[]
|
||||
>(() => {
|
||||
const base = chipRowsFromLabels(m.create.communitySize.communitySizes);
|
||||
const base = chipRowsFromLabels(cs.communitySizes);
|
||||
const selected = new Set(state.selectedCommunitySizeIds ?? []);
|
||||
return base.map((opt) => ({
|
||||
...opt,
|
||||
@@ -90,16 +58,6 @@ export function CommunitySizeSelectScreen() {
|
||||
);
|
||||
}, [state.selectedCommunitySizeIds]);
|
||||
|
||||
const communityCustomHandlers = useMemo(
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
setCommunitySizeOptions,
|
||||
"Unselected",
|
||||
markCreateFlowInteraction,
|
||||
),
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
const persistSelection = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setCommunitySizeOptions(next);
|
||||
@@ -123,18 +81,13 @@ export function CommunitySizeSelectScreen() {
|
||||
persistSelection(next);
|
||||
};
|
||||
|
||||
const multiLabel = t("multiSelect.label");
|
||||
const addText = t("multiSelect.addButtonText");
|
||||
|
||||
const multiSelectBlock = (
|
||||
<MultiSelect
|
||||
label={multiLabel}
|
||||
size="S"
|
||||
formHeader={false}
|
||||
size="M"
|
||||
options={communitySizeOptions}
|
||||
onChipClick={handleCommunitySizeClick}
|
||||
{...communityCustomHandlers}
|
||||
addButton={true}
|
||||
addButtonText={addText}
|
||||
addButton={false}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -143,29 +96,22 @@ export function CommunitySizeSelectScreen() {
|
||||
variant="centeredNarrow"
|
||||
contentTopBelowMd="space-1400"
|
||||
>
|
||||
{mdUp ? (
|
||||
<div className="flex w-full max-w-[1280px] items-center justify-center gap-[var(--measures-spacing-1200,48px)]">
|
||||
<div className="flex max-w-[640px] min-h-px min-w-px flex-[1_0_0] flex-col items-start justify-center gap-[var(--measures-spacing-200,8px)] py-[12px]">
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
justification="left"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex max-w-[640px] min-h-px min-w-px flex-[1_0_0] flex-col items-start gap-[var(--measures-spacing-800,32px)]">
|
||||
{multiSelectBlock}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full max-w-[640px] flex-col items-start gap-[var(--measures-spacing-400,16px)]">
|
||||
<div className="flex w-full min-w-0 flex-col items-start gap-[var(--measures-spacing-400,16px)] md:max-w-[640px] lg:max-w-[1328px] lg:flex-row lg:flex-nowrap lg:items-center lg:justify-center lg:gap-[var(--measures-spacing-1200,48px)]">
|
||||
<div
|
||||
className={`flex flex-col items-start gap-[var(--measures-spacing-200,8px)] lg:flex-1 lg:justify-center lg:py-[12px] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||
>
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
title={cs.header.title}
|
||||
description={cs.header.description}
|
||||
justification="left"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`flex flex-col items-start gap-[var(--measures-spacing-800,32px)] lg:flex-1 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||
>
|
||||
{multiSelectBlock}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CreateFlowStepShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
} from "react";
|
||||
import MultiSelect from "../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||
|
||||
function createListCustomHandlers(
|
||||
setList: Dispatch<SetStateAction<ChipOption[]>>,
|
||||
@@ -73,28 +73,38 @@ function applySavedSelection(
|
||||
);
|
||||
}
|
||||
|
||||
/** Create Community — frame 5 (Figma 20094-41317). */
|
||||
function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
||||
return options
|
||||
.filter((o) => o.state === "Selected")
|
||||
.map((o) => o.id);
|
||||
}
|
||||
|
||||
/** Create Community step 3 — Figma `20094:18244` (responsive grid + column caps via `createFlowLayoutTokens`). */
|
||||
export function CommunityStructureSelectScreen() {
|
||||
const m = useMessages();
|
||||
const cs = m.create.communityStructure;
|
||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.communityStructure");
|
||||
|
||||
const [organizationTypeOptions, setOrganizationTypeOptions] = useState<
|
||||
ChipOption[]
|
||||
>(() =>
|
||||
applySavedSelection(
|
||||
chipRowsFromLabels(m.create.communityStructure.organizationTypes),
|
||||
chipRowsFromLabels(cs.organizationTypes),
|
||||
state.selectedOrganizationTypeIds,
|
||||
),
|
||||
);
|
||||
|
||||
const [governanceStyleOptions, setGovernanceStyleOptions] = useState<
|
||||
ChipOption[]
|
||||
>(() =>
|
||||
const [scaleOptions, setScaleOptions] = useState<ChipOption[]>(() =>
|
||||
applySavedSelection(
|
||||
chipRowsFromLabels(m.create.communityStructure.governanceStyles),
|
||||
state.selectedGovernanceStyleIds,
|
||||
chipRowsFromLabels(cs.scaleOptions),
|
||||
state.selectedScaleIds,
|
||||
),
|
||||
);
|
||||
|
||||
const [maturityOptions, setMaturityOptions] = useState<ChipOption[]>(() =>
|
||||
applySavedSelection(
|
||||
chipRowsFromLabels(cs.maturityOptions),
|
||||
state.selectedMaturityIds,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -105,10 +115,14 @@ export function CommunityStructureSelectScreen() {
|
||||
}, [state.selectedOrganizationTypeIds]);
|
||||
|
||||
useEffect(() => {
|
||||
setGovernanceStyleOptions((prev) =>
|
||||
applySavedSelection(prev, state.selectedGovernanceStyleIds),
|
||||
setScaleOptions((prev) => applySavedSelection(prev, state.selectedScaleIds));
|
||||
}, [state.selectedScaleIds]);
|
||||
|
||||
useEffect(() => {
|
||||
setMaturityOptions((prev) =>
|
||||
applySavedSelection(prev, state.selectedMaturityIds),
|
||||
);
|
||||
}, [state.selectedGovernanceStyleIds]);
|
||||
}, [state.selectedMaturityIds]);
|
||||
|
||||
const organizationCustomHandlers = useMemo(
|
||||
() =>
|
||||
@@ -119,10 +133,19 @@ export function CommunityStructureSelectScreen() {
|
||||
),
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
const governanceCustomHandlers = useMemo(
|
||||
const scaleCustomHandlers = useMemo(
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
setGovernanceStyleOptions,
|
||||
setScaleOptions,
|
||||
"Unselected",
|
||||
markCreateFlowInteraction,
|
||||
),
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
const maturityCustomHandlers = useMemo(
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
setMaturityOptions,
|
||||
"Unselected",
|
||||
markCreateFlowInteraction,
|
||||
),
|
||||
@@ -132,75 +155,100 @@ export function CommunityStructureSelectScreen() {
|
||||
const persistOrg = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setOrganizationTypeOptions(next);
|
||||
updateState({
|
||||
selectedOrganizationTypeIds: next
|
||||
.filter((o) => o.state === "Selected")
|
||||
.map((o) => o.id),
|
||||
});
|
||||
updateState({ selectedOrganizationTypeIds: selectedIdsFromOptions(next) });
|
||||
};
|
||||
|
||||
const persistGov = (next: ChipOption[]) => {
|
||||
const persistScale = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setGovernanceStyleOptions(next);
|
||||
updateState({
|
||||
selectedGovernanceStyleIds: next
|
||||
.filter((o) => o.state === "Selected")
|
||||
.map((o) => o.id),
|
||||
});
|
||||
setScaleOptions(next);
|
||||
updateState({ selectedScaleIds: selectedIdsFromOptions(next) });
|
||||
};
|
||||
|
||||
const persistMaturity = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setMaturityOptions(next);
|
||||
updateState({ selectedMaturityIds: selectedIdsFromOptions(next) });
|
||||
};
|
||||
|
||||
const handleOrganizationTypeClick = (chipId: string) => {
|
||||
const next: ChipOption[] = organizationTypeOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
}
|
||||
: opt,
|
||||
persistOrg(
|
||||
organizationTypeOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
persistOrg(next);
|
||||
};
|
||||
|
||||
const handleGovernanceStyleClick = (chipId: string) => {
|
||||
const next: ChipOption[] = governanceStyleOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
}
|
||||
: opt,
|
||||
const handleScaleClick = (chipId: string) => {
|
||||
persistScale(
|
||||
scaleOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
persistGov(next);
|
||||
};
|
||||
|
||||
const multiLabel = t("multiSelect.label");
|
||||
const addText = t("multiSelect.addButtonText");
|
||||
const handleMaturityClick = (chipId: string) => {
|
||||
persistMaturity(
|
||||
maturityOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const multiSelectBlock = (
|
||||
<>
|
||||
<MultiSelect
|
||||
label={multiLabel}
|
||||
label={cs.organizationMultiSelect.label}
|
||||
showHelpIcon
|
||||
size="S"
|
||||
options={organizationTypeOptions}
|
||||
onChipClick={handleOrganizationTypeClick}
|
||||
{...organizationCustomHandlers}
|
||||
addButton={true}
|
||||
addButtonText={addText}
|
||||
addButton
|
||||
addButtonText={cs.organizationMultiSelect.addButtonText}
|
||||
/>
|
||||
<MultiSelect
|
||||
label={multiLabel}
|
||||
label={cs.scaleMultiSelect.label}
|
||||
showHelpIcon
|
||||
size="S"
|
||||
options={governanceStyleOptions}
|
||||
onChipClick={handleGovernanceStyleClick}
|
||||
{...governanceCustomHandlers}
|
||||
addButton={true}
|
||||
addButtonText={addText}
|
||||
options={scaleOptions}
|
||||
onChipClick={handleScaleClick}
|
||||
{...scaleCustomHandlers}
|
||||
addButton
|
||||
addButtonText={cs.scaleMultiSelect.addButtonText}
|
||||
/>
|
||||
<MultiSelect
|
||||
label={cs.maturityMultiSelect.label}
|
||||
showHelpIcon
|
||||
size="S"
|
||||
options={maturityOptions}
|
||||
onChipClick={handleMaturityClick}
|
||||
{...maturityCustomHandlers}
|
||||
addButton
|
||||
addButtonText={cs.maturityMultiSelect.addButtonText}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -210,29 +258,22 @@ export function CommunityStructureSelectScreen() {
|
||||
variant="centeredNarrow"
|
||||
contentTopBelowMd="space-1400"
|
||||
>
|
||||
{mdUp ? (
|
||||
<div className="flex w-full max-w-[1280px] items-center justify-center gap-[var(--measures-spacing-1200,48px)]">
|
||||
<div className="flex max-w-[640px] min-h-px min-w-px flex-[1_0_0] flex-col items-start justify-center gap-[var(--measures-spacing-200,8px)] py-[12px]">
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
justification="left"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex max-w-[640px] min-h-px min-w-px flex-[1_0_0] flex-col items-start gap-[var(--measures-spacing-800,32px)]">
|
||||
{multiSelectBlock}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full max-w-[640px] flex-col items-start gap-[var(--measures-spacing-400,16px)]">
|
||||
<div className="flex w-full min-w-0 flex-col items-start gap-[var(--measures-spacing-400,16px)] md:max-w-[640px] lg:max-w-[1328px] lg:flex-row lg:flex-nowrap lg:items-center lg:justify-center lg:gap-[var(--measures-spacing-1200,48px)]">
|
||||
<div
|
||||
className={`flex flex-col items-start gap-[var(--measures-spacing-200,8px)] lg:flex-1 lg:justify-center lg:py-[12px] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||
>
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
title={cs.header.title}
|
||||
description={cs.header.description}
|
||||
justification="left"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`flex flex-col items-start gap-[var(--measures-spacing-800,32px)] lg:flex-1 ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||
>
|
||||
{multiSelectBlock}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CreateFlowStepShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||
|
||||
export function ConfirmStakeholdersScreen() {
|
||||
const { markCreateFlowInteraction } = useCreateFlow();
|
||||
@@ -50,7 +51,9 @@ export function ConfirmStakeholdersScreen() {
|
||||
variant="centeredNarrowBottomPad"
|
||||
contentTopBelowMd="space-1400"
|
||||
>
|
||||
<div className="flex w-full max-w-[640px] flex-col items-start gap-[var(--measures-spacing-300,12px)]">
|
||||
<div
|
||||
className={`flex flex-col items-start gap-[var(--measures-spacing-300,12px)] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-[var(--measures-spacing-200,8px)] py-[12px]">
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("title")}
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, type HTMLInputTypeAttribute } from "react";
|
||||
import TextInput from "../../../components/controls/TextInput";
|
||||
import type { HeaderLockupJustificationValue } from "../../../components/type/HeaderLockup/HeaderLockup.types";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import {
|
||||
CreateFlowStepShell,
|
||||
type CreateFlowContentTopBelowMd,
|
||||
} from "../../components/CreateFlowStepShell";
|
||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||
import type { CreateFlowTextStateField } from "../../types";
|
||||
|
||||
type Props = {
|
||||
messageNamespace: string;
|
||||
stateField: CreateFlowTextStateField;
|
||||
maxLength: number;
|
||||
/** Figma Flow — Text (`20094:41243`): main column `items-center` + horizontal padding token. */
|
||||
mainAlign?: "start" | "center";
|
||||
inputType?: HTMLInputTypeAttribute;
|
||||
showCharacterCount?: boolean;
|
||||
headerJustification?: HeaderLockupJustificationValue;
|
||||
/** Top spacing under top chrome (`CreateFlowStepShell` / `CreateFlowContentTopBelowMd`). */
|
||||
contentTopBelowMd?: CreateFlowContentTopBelowMd;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -22,6 +34,11 @@ export function CreateFlowTextFieldScreen({
|
||||
messageNamespace,
|
||||
stateField,
|
||||
maxLength,
|
||||
mainAlign = "start",
|
||||
inputType = "text",
|
||||
showCharacterCount = true,
|
||||
headerJustification = "left",
|
||||
contentTopBelowMd = "space-1400",
|
||||
}: Props) {
|
||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
@@ -42,20 +59,35 @@ export function CreateFlowTextFieldScreen({
|
||||
}, [state, stateField]);
|
||||
|
||||
const characterCount = value.length;
|
||||
const hint = t("characterCountTemplate")
|
||||
.replace("{current}", String(characterCount))
|
||||
.replace("{max}", String(maxLength));
|
||||
const hint =
|
||||
showCharacterCount === false
|
||||
? false
|
||||
: t("characterCountTemplate")
|
||||
.replace("{current}", String(characterCount))
|
||||
.replace("{max}", String(maxLength));
|
||||
|
||||
const mainItems =
|
||||
mainAlign === "center" ? "items-center" : "items-start";
|
||||
|
||||
return (
|
||||
<CreateFlowStepShell variant="centeredNarrow">
|
||||
<div className="flex w-full max-w-[640px] flex-col items-start gap-[18px]">
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("title")}
|
||||
description={t("description")}
|
||||
justification="left"
|
||||
/>
|
||||
<CreateFlowStepShell
|
||||
variant="centeredNarrow"
|
||||
contentTopBelowMd={contentTopBelowMd}
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col gap-[18px] ${mainItems} ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||
>
|
||||
<div className="w-full">
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("title")}
|
||||
description={t("description")}
|
||||
justification={headerJustification}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<TextInput
|
||||
className="!transition-none"
|
||||
type={inputType}
|
||||
placeholder={t("placeholder")}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import Upload from "../../../components/controls/Upload";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||
|
||||
/** Create Community — frame 6 (Figma 20094-41524). */
|
||||
/** Create Community — Figma Flow — Upload `20094:41524`. */
|
||||
export function CommunityUploadScreen() {
|
||||
const m = useMessages();
|
||||
const u = m.create.communityUpload;
|
||||
const { markCreateFlowInteraction } = useCreateFlow();
|
||||
const mdUp = useCreateFlowMdUp();
|
||||
const t = useTranslation("create.communityUpload");
|
||||
|
||||
const handleUploadClick = () => {
|
||||
markCreateFlowInteraction();
|
||||
@@ -22,16 +22,21 @@ export function CommunityUploadScreen() {
|
||||
variant="centeredNarrow"
|
||||
contentTopBelowMd="space-1400"
|
||||
>
|
||||
<div className="flex w-full max-w-[640px] flex-col items-center gap-[18px]">
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("title")}
|
||||
description={t("description")}
|
||||
justification={mdUp ? "center" : "left"}
|
||||
/>
|
||||
<div className="w-full max-w-[474px]">
|
||||
<div
|
||||
className={`flex flex-col items-center gap-[18px] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||
>
|
||||
<div className="w-full">
|
||||
<CreateFlowHeaderLockup
|
||||
title={u.title}
|
||||
description={u.description}
|
||||
justification="center"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Upload
|
||||
active={true}
|
||||
showHelpIcon={true}
|
||||
showHelpIcon={false}
|
||||
hintText={u.hintText}
|
||||
onClick={handleUploadClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+8
-5
@@ -16,7 +16,7 @@ export type CreateFlowStep =
|
||||
| "community-context"
|
||||
| "community-structure"
|
||||
| "community-upload"
|
||||
| "community-reflection"
|
||||
| "community-save"
|
||||
| "review"
|
||||
| "cards"
|
||||
| "right-rail"
|
||||
@@ -29,7 +29,7 @@ export type CreateFlowTextStateField =
|
||||
| "title"
|
||||
| "summary"
|
||||
| "communityContext"
|
||||
| "communityReflection";
|
||||
| "communitySaveEmail";
|
||||
|
||||
/**
|
||||
* Flow state for inputs across create-flow steps.
|
||||
@@ -41,13 +41,16 @@ export interface CreateFlowState {
|
||||
summary?: string;
|
||||
/** Additional copy fields for multi-step Create Community text frames (Figma). */
|
||||
communityContext?: string;
|
||||
communityReflection?: string;
|
||||
/** Email collected on the “Save your progress” step (Figma Flow — Text `20097:14948`). */
|
||||
communitySaveEmail?: string;
|
||||
/** Selected chip ids from `community-size` (MultiSelect). */
|
||||
selectedCommunitySizeIds?: string[];
|
||||
/** Selected chip ids from `community-structure` (organization types). */
|
||||
selectedOrganizationTypeIds?: string[];
|
||||
/** Selected chip ids from `community-structure` (governance styles). */
|
||||
selectedGovernanceStyleIds?: string[];
|
||||
/** Selected chip ids from `community-structure` (scale). */
|
||||
selectedScaleIds?: string[];
|
||||
/** Selected chip ids from `community-structure` (maturity). */
|
||||
selectedMaturityIds?: string[];
|
||||
currentStep?: CreateFlowStep;
|
||||
/** Section drafts; structure will tighten as steps persist real shapes. */
|
||||
sections?: Record<string, unknown>[];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CreateFlowState } from "../types";
|
||||
import { migrateLegacyCreateFlowState } from "../../../lib/create/migrateLegacyCreateFlowState";
|
||||
|
||||
/** Anonymous in-progress create flow (local only until magic-link transfer). */
|
||||
export const CREATE_FLOW_ANONYMOUS_KEY = "create-flow-anonymous" as const;
|
||||
@@ -23,8 +24,10 @@ export function readAnonymousCreateFlowState(): CreateFlowState {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY);
|
||||
if (!raw) return {};
|
||||
const parsed = JSON.parse(raw) as CreateFlowState;
|
||||
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
return typeof parsed === "object" && parsed !== null
|
||||
? migrateLegacyCreateFlowState(parsed)
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ProportionBarState } from "../../components/progress/ProportionBar/ProportionBar.types";
|
||||
import type { CreateFlowStep } from "../types";
|
||||
import { FLOW_STEP_ORDER, getStepIndex } from "./flowSteps";
|
||||
|
||||
/**
|
||||
* One `ProportionBarState` per index in `FLOW_STEP_ORDER` (same length).
|
||||
* Third Create Community step (`community-structure`) uses `1-2` per Figma.
|
||||
*/
|
||||
const PROPORTION_BY_STEP_INDEX: readonly ProportionBarState[] = [
|
||||
"1-0", // informational
|
||||
"1-1", // community-name
|
||||
"1-2", // community-structure
|
||||
"1-3", // community-context
|
||||
"1-4", // community-size
|
||||
"1-5", // community-upload
|
||||
"2-0", // community-save
|
||||
"2-0", // review (Figma Flow — Review `19706:12135`: same segment fill as end of Create Community)
|
||||
"2-2", // cards
|
||||
"3-0", // right-rail
|
||||
"3-1", // confirm-stakeholders
|
||||
"3-2", // final-review
|
||||
"3-2", // completed
|
||||
] as const;
|
||||
|
||||
if (PROPORTION_BY_STEP_INDEX.length !== FLOW_STEP_ORDER.length) {
|
||||
throw new Error(
|
||||
"createFlowProportionProgress: PROPORTION_BY_STEP_INDEX length must match FLOW_STEP_ORDER",
|
||||
);
|
||||
}
|
||||
|
||||
export function getProportionBarProgressForCreateFlowStep(
|
||||
step: CreateFlowStep | null | undefined,
|
||||
): ProportionBarState {
|
||||
const idx = getStepIndex(step);
|
||||
if (idx < 0) return "1-0";
|
||||
return PROPORTION_BY_STEP_INDEX[idx] ?? "1-0";
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
||||
CreateFlowStep,
|
||||
CreateFlowScreenDefinition
|
||||
> = {
|
||||
/** Figma: Flow — Informational (node 20094-16005). */
|
||||
informational: {
|
||||
layoutKind: "informational",
|
||||
figmaNodeId: "20094-16005",
|
||||
@@ -49,7 +50,7 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
||||
},
|
||||
"community-size": {
|
||||
layoutKind: "select",
|
||||
figmaNodeId: "20094-18244",
|
||||
figmaNodeId: "20094-41317",
|
||||
messageNamespace: "create.communitySize",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
@@ -61,7 +62,7 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
||||
},
|
||||
"community-structure": {
|
||||
layoutKind: "select",
|
||||
figmaNodeId: "20094-41317",
|
||||
figmaNodeId: "20094-18244",
|
||||
messageNamespace: "create.communityStructure",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
@@ -71,10 +72,10 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
||||
messageNamespace: "create.communityUpload",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"community-reflection": {
|
||||
"community-save": {
|
||||
layoutKind: "text",
|
||||
figmaNodeId: "20097-14948",
|
||||
messageNamespace: "create.communityReflection",
|
||||
messageNamespace: "create.communitySave",
|
||||
centeredBodyBelowMd: true,
|
||||
},
|
||||
review: {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Single source of truth for step order and navigation helpers.
|
||||
* Order matches Figma Create Community (frames 1–8) then later stages.
|
||||
* `community-structure` precedes `community-context` and `community-size` (Figma frame 3 vs 5 swap).
|
||||
*/
|
||||
|
||||
import type { CreateFlowStep } from "../types";
|
||||
@@ -13,11 +14,11 @@ import type { CreateFlowStep } from "../types";
|
||||
export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [
|
||||
"informational",
|
||||
"community-name",
|
||||
"community-size",
|
||||
"community-context",
|
||||
"community-structure",
|
||||
"community-context",
|
||||
"community-size",
|
||||
"community-upload",
|
||||
"community-reflection",
|
||||
"community-save",
|
||||
"review",
|
||||
"cards",
|
||||
"right-rail",
|
||||
|
||||
Reference in New Issue
Block a user