From 92149d9fb06829745d1ed46d5ac6d443017fe993 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:15:02 -0600 Subject: [PATCH 01/15] Skip create save page when logged in --- app/create/CreateFlowLayoutClient.tsx | 15 ++++++++++++++- app/create/hooks/useCreateFlowNavigation.ts | 9 ++++++--- app/create/utils/flowSteps.ts | 19 +++++++++++++++++-- tests/unit/flowSteps.test.ts | 13 +++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/app/create/CreateFlowLayoutClient.tsx b/app/create/CreateFlowLayoutClient.tsx index 2ee4581..fb83700 100644 --- a/app/create/CreateFlowLayoutClient.tsx +++ b/app/create/CreateFlowLayoutClient.tsx @@ -93,13 +93,16 @@ function CreateFlowLayoutContent({ const router = useRouter(); const pathname = usePathname(); const { openLogin } = useAuthModal(); + const skipCommunitySave = sessionResolved && Boolean(sessionUser); const { currentStep, nextStep, previousStep, goToNextStep, goToPreviousStep, - } = useCreateFlowNavigation(); + } = useCreateFlowNavigation( + skipCommunitySave ? { skipCommunitySave: true } : undefined, + ); const { state, clearState, updateState } = useCreateFlow(); const { draftSaveBannerMessage, setDraftSaveBannerMessage } = useCreateFlowDraftSaveBanner(); @@ -240,6 +243,16 @@ function CreateFlowLayoutContent({ await runAuthenticatedExit(opts); }; + useEffect(() => { + if ( + sessionResolved && + sessionUser && + currentStep === "community-save" + ) { + router.replace("/create/review"); + } + }, [sessionResolved, sessionUser, currentStep, router]); + useEffect(() => { if (currentStep !== "community-save") { setCommunitySaveMagicLinkError(null); diff --git a/app/create/hooks/useCreateFlowNavigation.ts b/app/create/hooks/useCreateFlowNavigation.ts index 74f5c4e..848e723 100644 --- a/app/create/hooks/useCreateFlowNavigation.ts +++ b/app/create/hooks/useCreateFlowNavigation.ts @@ -4,6 +4,7 @@ import { usePathname, useRouter } from "next/navigation"; import { useCallback } from "react"; import type { CreateFlowStep } from "../types"; import { + type CreateFlowNavigationOptions, getNextStep, getPreviousStep, parseCreateFlowScreenFromPathname, @@ -26,7 +27,9 @@ const blurActiveElement = (): void => { * * Resolves the active step from `/create/{screenId}` via {@link parseCreateFlowScreenFromPathname} (flowSteps). */ -export function useCreateFlowNavigation(): { +export function useCreateFlowNavigation( + options?: CreateFlowNavigationOptions, +): { currentStep: CreateFlowStep | null; goToNextStep: () => void; goToPreviousStep: () => void; @@ -41,8 +44,8 @@ export function useCreateFlowNavigation(): { const validStep = parseCreateFlowScreenFromPathname(pathname ?? null); - const nextStep = getNextStep(validStep); - const previousStep = getPreviousStep(validStep); + const nextStep = getNextStep(validStep, options); + const previousStep = getPreviousStep(validStep, options); const goToNextStep = useCallback(() => { blurActiveElement(); diff --git a/app/create/utils/flowSteps.ts b/app/create/utils/flowSteps.ts index b8138df..93f1331 100644 --- a/app/create/utils/flowSteps.ts +++ b/app/create/utils/flowSteps.ts @@ -37,16 +37,26 @@ export const VALID_STEPS: readonly CreateFlowStep[] = FLOW_STEP_ORDER; */ export const FIRST_STEP: CreateFlowStep = FLOW_STEP_ORDER[0]; +/** Options for navigation when the email / magic-link save step is not shown (signed-in users). */ +export type CreateFlowNavigationOptions = { + skipCommunitySave?: boolean; +}; + /** * Returns the next step in the flow, or null if current is last/invalid */ export function getNextStep( currentStep: CreateFlowStep | null | undefined, + options?: CreateFlowNavigationOptions, ): CreateFlowStep | null { if (!currentStep) return null; const index = FLOW_STEP_ORDER.indexOf(currentStep); if (index === -1 || index === FLOW_STEP_ORDER.length - 1) return null; - return FLOW_STEP_ORDER[index + 1] as CreateFlowStep; + const next = FLOW_STEP_ORDER[index + 1] as CreateFlowStep; + if (options?.skipCommunitySave && next === "community-save") { + return getNextStep("community-save", options); + } + return next; } /** @@ -54,11 +64,16 @@ export function getNextStep( */ export function getPreviousStep( currentStep: CreateFlowStep | null | undefined, + options?: CreateFlowNavigationOptions, ): CreateFlowStep | null { if (!currentStep) return null; const index = FLOW_STEP_ORDER.indexOf(currentStep); if (index <= 0) return null; - return FLOW_STEP_ORDER[index - 1] as CreateFlowStep; + const prev = FLOW_STEP_ORDER[index - 1] as CreateFlowStep; + if (options?.skipCommunitySave && prev === "community-save") { + return getPreviousStep("community-save", options); + } + return prev; } /** diff --git a/tests/unit/flowSteps.test.ts b/tests/unit/flowSteps.test.ts index 1cf4b9f..04e451c 100644 --- a/tests/unit/flowSteps.test.ts +++ b/tests/unit/flowSteps.test.ts @@ -55,4 +55,17 @@ describe("flowSteps", () => { expect(getNextStep("community-structure")).toBe("community-context"); expect(getNextStep("community-context")).toBe("community-size"); }); + + it("skipCommunitySave bridges upload → review and review → upload", () => { + const opts = { skipCommunitySave: true } as const; + expect(getNextStep("community-upload", opts)).toBe("review"); + expect(getPreviousStep("review", opts)).toBe("community-upload"); + }); + + it("skipCommunitySave does not change steps outside the save segment", () => { + const opts = { skipCommunitySave: true } as const; + expect(getNextStep("community-size", opts)).toBe("community-upload"); + expect(getNextStep("review", opts)).toBe("cards"); + expect(getPreviousStep("cards", opts)).toBe("review"); + }); }); From b15f0d62262cf1be7c6b3c6a438bdcae4a24c0f7 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:23:48 -0600 Subject: [PATCH 02/15] Align DB with create community stage --- .../controls/Upload/Upload.view.tsx | 4 +- app/create/context/CreateFlowContext.tsx | 14 ++- .../select/CommunityStructureSelectScreen.tsx | 118 +++++++++++++++--- app/create/types.ts | 19 +++ lib/server/validation/createFlowSchemas.ts | 16 +++ tests/unit/createFlowValidation.test.ts | 23 ++++ 6 files changed, 170 insertions(+), 24 deletions(-) diff --git a/app/components/controls/Upload/Upload.view.tsx b/app/components/controls/Upload/Upload.view.tsx index 96748bc..8409881 100644 --- a/app/components/controls/Upload/Upload.view.tsx +++ b/app/components/controls/Upload/Upload.view.tsx @@ -49,8 +49,8 @@ function UploadView({ /> )} - {/* Upload container */} -
+ {/* Upload container — max width per create-flow spec */} +
{/* Upload button */}
) : currentStep === "community-name" && nextStep ? ( -
- - -
+ ) : currentStep === "community-save" && nextStep ? (
+ ) : currentStep === "core-values" && nextStep ? ( + ) : nextStep ? ( + + {" "} + {cv.header.descriptionTrail} + + + ); + + return ( + + } + > + + + ); +} diff --git a/app/create/types.ts b/app/create/types.ts index 9aae00a..ef86b0c 100644 --- a/app/create/types.ts +++ b/app/create/types.ts @@ -18,6 +18,7 @@ export type CreateFlowStep = | "community-upload" | "community-save" | "review" + | "core-values" | "cards" | "right-rail" | "confirm-stakeholders" @@ -70,6 +71,10 @@ export interface CreateFlowState { scale?: CommunityStructureChipSnapshotRow[]; maturity?: CommunityStructureChipSnapshotRow[]; }; + /** Create Custom — core values step (max five `selectedCoreValueIds`). */ + selectedCoreValueIds?: string[]; + /** Full chip rows for core values (custom labels). */ + coreValuesChipsSnapshot?: CommunityStructureChipSnapshotRow[]; currentStep?: CreateFlowStep; /** Section drafts; structure will tighten as steps persist real shapes. */ sections?: Record[]; diff --git a/app/create/utils/createFlowProportionProgress.ts b/app/create/utils/createFlowProportionProgress.ts index 9b649a1..2541e39 100644 --- a/app/create/utils/createFlowProportionProgress.ts +++ b/app/create/utils/createFlowProportionProgress.ts @@ -15,6 +15,7 @@ const PROPORTION_BY_STEP_INDEX: readonly ProportionBarState[] = [ "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-0", // core-values (same segment as review / end of Create Community) "2-2", // cards "3-0", // right-rail "3-1", // confirm-stakeholders diff --git a/app/create/utils/createFlowScreenRegistry.ts b/app/create/utils/createFlowScreenRegistry.ts index 9959541..35719b2 100644 --- a/app/create/utils/createFlowScreenRegistry.ts +++ b/app/create/utils/createFlowScreenRegistry.ts @@ -84,6 +84,12 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record< messageNamespace: "create.review", centeredBodyBelowMd: false, }, + "core-values": { + layoutKind: "select", + figmaNodeId: "20264-68378", + messageNamespace: "create.coreValues", + centeredBodyBelowMd: false, + }, cards: { layoutKind: "card", figmaNodeId: "TBD-cards", diff --git a/app/create/utils/flowSteps.ts b/app/create/utils/flowSteps.ts index 93f1331..818a3fc 100644 --- a/app/create/utils/flowSteps.ts +++ b/app/create/utils/flowSteps.ts @@ -20,6 +20,7 @@ export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [ "community-upload", "community-save", "review", + "core-values", "cards", "right-rail", "confirm-stakeholders", diff --git a/lib/server/validation/createFlowSchemas.ts b/lib/server/validation/createFlowSchemas.ts index 37d24eb..0ecfdf7 100644 --- a/lib/server/validation/createFlowSchemas.ts +++ b/lib/server/validation/createFlowSchemas.ts @@ -51,6 +51,10 @@ export const createFlowStateSchema = z selectedMaturityIds: z.array(z.string()).optional(), communityStructureChipSnapshots: communityStructureChipSnapshotsSchema.optional(), + selectedCoreValueIds: z.array(z.string()).max(200).optional(), + coreValuesChipsSnapshot: z + .array(communityStructureChipSnapshotRowSchema) + .optional(), currentStep: createFlowStepSchema.optional(), sections: z.array(z.unknown()).optional(), stakeholders: z.array(z.unknown()).optional(), diff --git a/messages/en/create/coreValues.json b/messages/en/create/coreValues.json new file mode 100644 index 0000000..56e120d --- /dev/null +++ b/messages/en/create/coreValues.json @@ -0,0 +1,75 @@ +{ + "header": { + "title": "Choose up to five Core Values", + "descriptionLead": "What does the community hold most dear? You can also combine or", + "addLink": "add", + "descriptionTrail": "new values to the list." + }, + "multiSelect": { + "addButtonText": "Add value" + }, + "values": [ + "Accessibility", + "Accountability", + "Adaptability", + "Agency", + "Altruism", + "Anti-oppression", + "Autonomy", + "Capacity Building", + "Collaboration", + "Common Ownership", + "Community Care", + "Conflict Resolution", + "Consent", + "Consensus", + "Constructive Feedback", + "Cooperation", + "Copyleft", + "Decentralization", + "Direct Action", + "Diversity", + "Documentation", + "Education", + "Empathy", + "Empowerment", + "Equity", + "Experimentation", + "Fairness", + "Fair Remuneration", + "Flexibility", + "Forkability", + "Freedom", + "Freedom of Information", + "Generosity", + "Harm Reduction", + "Holism", + "Holocracy", + "Honesty", + "Horizontalism", + "Humility", + "Inclusion", + "Inclusivity", + "Independence", + "Innovation", + "Integrity", + "Interdependence", + "Interoperability", + "Intersectionality", + "Justice", + "Knowledge Sharing", + "Labor Rights", + "Leadership", + "Learning", + "Liberty", + "Localism", + "Maintenance", + "Mentorship", + "Meritocracy", + "Mutual Aid", + "Non-violence", + "Open Source", + "Openness", + "Participation" + ] +} diff --git a/messages/en/create/footer.json b/messages/en/create/footer.json index 3698c2a..1708b87 100644 --- a/messages/en/create/footer.json +++ b/messages/en/create/footer.json @@ -10,5 +10,6 @@ "confirmDescription": "Confirm description", "confirmMembers": "Confirm members", "finalizeCommunityRule": "Finalize CommunityRule", - "confirmStakeholders": "Confirm Stakeholders" + "confirmStakeholders": "Confirm Stakeholders", + "confirmCoreValues": "Confirm values" } diff --git a/messages/en/index.ts b/messages/en/index.ts index e1044fd..57455fe 100644 --- a/messages/en/index.ts +++ b/messages/en/index.ts @@ -27,6 +27,7 @@ import createCommunityStructure from "./create/communityStructure.json"; import createCommunityUpload from "./create/communityUpload.json"; import createCommunitySave from "./create/communitySave.json"; import createReview from "./create/review.json"; +import createCoreValues from "./create/coreValues.json"; import createConfirmStakeholders from "./create/confirmStakeholders.json"; import createFinalReview from "./create/finalReview.json"; import createCompleted from "./create/completed.json"; @@ -68,6 +69,7 @@ export default { communityUpload: createCommunityUpload, communitySave: createCommunitySave, review: createReview, + coreValues: createCoreValues, confirmStakeholders: createConfirmStakeholders, finalReview: createFinalReview, completed: createCompleted, diff --git a/tests/unit/createFlowProportionProgress.test.ts b/tests/unit/createFlowProportionProgress.test.ts index 3b463b7..aa0534c 100644 --- a/tests/unit/createFlowProportionProgress.test.ts +++ b/tests/unit/createFlowProportionProgress.test.ts @@ -17,10 +17,13 @@ describe("getProportionBarProgressForCreateFlowStep", () => { ); }); - it("uses 2-0 on community-save and review (end of Create Community segment)", () => { + it("uses 2-0 on community-save, review, and core-values (Create Community segment / same fill)", () => { expect(getProportionBarProgressForCreateFlowStep("community-save")).toBe( "2-0", ); expect(getProportionBarProgressForCreateFlowStep("review")).toBe("2-0"); + expect(getProportionBarProgressForCreateFlowStep("core-values")).toBe( + "2-0", + ); }); }); diff --git a/tests/unit/flowSteps.test.ts b/tests/unit/flowSteps.test.ts index 04e451c..cfc0cec 100644 --- a/tests/unit/flowSteps.test.ts +++ b/tests/unit/flowSteps.test.ts @@ -36,6 +36,7 @@ describe("flowSteps", () => { it("isValidStep reflects FLOW_STEP_ORDER membership", () => { expect(isValidStep("community-size")).toBe(true); expect(isValidStep("confirm-stakeholders")).toBe(true); + expect(isValidStep("core-values")).toBe(true); expect(isValidStep("nope")).toBe(false); expect(isValidStep(null)).toBe(false); }); @@ -65,7 +66,7 @@ describe("flowSteps", () => { it("skipCommunitySave does not change steps outside the save segment", () => { const opts = { skipCommunitySave: true } as const; expect(getNextStep("community-size", opts)).toBe("community-upload"); - expect(getNextStep("review", opts)).toBe("cards"); - expect(getPreviousStep("cards", opts)).toBe("review"); + expect(getNextStep("review", opts)).toBe("core-values"); + expect(getPreviousStep("cards", opts)).toBe("core-values"); }); }); From eedb70f9f3a1a387022a9a68e203b2dff96ac0c1 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:13:28 -0600 Subject: [PATCH 04/15] Implement core value modals --- .../modals/Create/Create.container.tsx | 2 + app/components/modals/Create/Create.types.ts | 6 + app/components/modals/Create/Create.view.tsx | 12 +- app/create/context/CreateFlowContext.tsx | 34 +- .../screens/select/CoreValuesSelectScreen.tsx | 244 +++++++++-- app/create/types.ts | 8 + .../utils/coreValueDetailsLocalStorage.ts | 57 +++ lib/create/buildPublishPayload.ts | 37 +- lib/server/validation/createFlowSchemas.ts | 8 + messages/en/create/coreValues.json | 378 +++++++++++++++--- stories/modals/Create.stories.js | 23 ++ .../CoreValuesSelectScreen.test.tsx | 42 ++ tests/components/Create.test.tsx | 12 + tests/unit/buildPublishPayload.test.ts | 25 +- tests/unit/createFlowValidation.test.ts | 19 + 15 files changed, 806 insertions(+), 101 deletions(-) create mode 100644 app/create/utils/coreValueDetailsLocalStorage.ts create mode 100644 tests/components/CoreValuesSelectScreen.test.tsx diff --git a/app/components/modals/Create/Create.container.tsx b/app/components/modals/Create/Create.container.tsx index 0390eb2..180f528 100644 --- a/app/components/modals/Create/Create.container.tsx +++ b/app/components/modals/Create/Create.container.tsx @@ -25,6 +25,7 @@ const CreateContainer = memo( className = "", ariaLabel, ariaLabelledBy, + backdropVariant = "default", }) => { const createRef = useRef(null); const overlayRef = useRef(null); @@ -132,6 +133,7 @@ const CreateContainer = memo( ariaLabelledBy={ariaLabelledBy} createRef={createRef} overlayRef={overlayRef} + backdropVariant={backdropVariant} /> ); }, diff --git a/app/components/modals/Create/Create.types.ts b/app/components/modals/Create/Create.types.ts index 4b6214a..7f0a7ee 100644 --- a/app/components/modals/Create/Create.types.ts +++ b/app/components/modals/Create/Create.types.ts @@ -27,6 +27,11 @@ export interface CreateProps { multiSelect?: boolean; upload?: boolean; proportion?: boolean; + /** + * Backdrop behind the dialog. `loginYellow` matches the Login modal’s blurred brand overlay. + * @default "default" + */ + backdropVariant?: "default" | "loginYellow"; } export interface CreateViewProps { @@ -51,4 +56,5 @@ export interface CreateViewProps { ariaLabelledBy?: string; createRef: React.RefObject; overlayRef: React.RefObject; + backdropVariant: "default" | "loginYellow"; } diff --git a/app/components/modals/Create/Create.view.tsx b/app/components/modals/Create/Create.view.tsx index 112d624..98933bb 100644 --- a/app/components/modals/Create/Create.view.tsx +++ b/app/components/modals/Create/Create.view.tsx @@ -6,6 +6,15 @@ import ModalFooter from "../../utility/ModalFooter"; import ModalHeader from "../../utility/ModalHeader"; import type { CreateViewProps } from "./Create.types"; +const backdropOverlayClasses: Record< + CreateViewProps["backdropVariant"], + string +> = { + default: "fixed inset-0 bg-black/50 z-[9998]", + loginYellow: + "fixed inset-0 z-[9998] bg-[var(--color-surface-inverse-brand-primary)]/85 backdrop-blur-md supports-[backdrop-filter]:bg-[var(--color-surface-inverse-brand-primary)]/75", +}; + export function CreateView({ isOpen, onClose, @@ -28,6 +37,7 @@ export function CreateView({ ariaLabelledBy, createRef, overlayRef, + backdropVariant, }: CreateViewProps) { if (!isOpen) return null; @@ -36,7 +46,7 @@ export function CreateView({ {/* Overlay */} + } + showBackButton={false} + showNextButton + onNext={handleModalConfirm} + nextButtonText={detailModal.addValueButton} + ariaLabel={modalChipLabel || "Core value details"} + > +
+