From dee2dd800e0e667a529a4282f2a1e2005b6a3fd4 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Fri, 1 May 2026 22:05:05 -0600 Subject: [PATCH] Add custom intervention modals --- .../CustomMethodCardFieldBlocksSummary.tsx | 279 ++++++++++++++ .../components/CustomMethodCardModalBody.tsx | 31 ++ .../CustomMethodCardPresetEditPlaceholder.tsx | 26 ++ .../CustomMethodCardWizard.container.tsx | 358 ++++++++++++++++++ .../CustomMethodCardWizard.types.ts | 120 ++++++ .../CustomMethodCardWizard.view.tsx | 95 +++++ ...CustomMethodCardWizardFieldBodies.view.tsx | 161 ++++++++ .../CustomMethodCardWizard/index.tsx | 2 + .../components/FinalReviewChipEditModal.tsx | 234 +++++++++--- .../useCustomMethodCardFieldBlocksChange.ts | 29 ++ .../create/hooks/useMethodCardDeckOrdering.ts | 27 +- .../card/CommunicationMethodsScreen.tsx | 165 ++++++-- .../screens/card/ConflictManagementScreen.tsx | 156 ++++++-- .../screens/card/MembershipMethodsScreen.tsx | 152 ++++++-- .../right-rail/DecisionApproachesScreen.tsx | 160 +++++++- app/(app)/create/types.ts | 14 + app/components/asset/icon/Icon.tsx | 16 +- app/components/asset/icon/image.svg | 11 + app/components/asset/icon/number.svg | 10 + app/components/asset/icon/tags.svg | 7 + app/components/asset/icon/text_block.svg | 6 + app/components/buttons/Vertical/Vertical.tsx | 49 +++ app/components/buttons/Vertical/index.tsx | 2 + .../cards/CardStack/CardStack.container.tsx | 8 + .../cards/CardStack/CardStack.types.ts | 14 +- .../cards/CardStack/CardStack.view.tsx | 141 ++++++- .../AddCustomField.container.tsx | 49 +++ .../AddCustomField/AddCustomField.types.ts | 18 + .../AddCustomField/AddCustomField.view.tsx | 133 +++++++ .../controls/AddCustomField/index.tsx | 5 + .../modals/Create/Create.container.tsx | 2 + app/components/modals/Create/Create.types.ts | 3 + app/components/modals/Create/Create.view.tsx | 2 + lib/create/applyFinalReviewChipEditPatch.ts | 16 +- lib/create/buildFinalReviewCategories.ts | 16 +- lib/create/buildPublishPayload.ts | 26 +- lib/create/customMethodCardFieldBlocks.ts | 75 ++++ lib/create/customMethodCardWizardConstants.ts | 2 + lib/create/finalReviewChipPresets.ts | 12 + lib/create/isCustomMethodCardId.ts | 13 + lib/create/mergePresetMethodsWithCustom.ts | 32 ++ lib/create/methodCardSelectionOrder.ts | 12 + .../publishedDocumentToCreateFlowState.ts | 9 +- .../removeMethodCardFromFacetSelection.ts | 86 +++++ lib/create/stripCustomRuleSelectionFields.ts | 2 + lib/server/validation/createFlowSchemas.ts | 10 + .../en/create/customRule/communication.json | 8 +- .../create/customRule/conflictManagement.json | 6 +- .../customRule/customMethodCardWizard.json | 79 ++++ .../create/customRule/decisionApproaches.json | 7 +- messages/en/create/customRule/membership.json | 8 +- messages/en/index.ts | 2 + stories/buttons/Vertical.stories.js | 65 ++++ stories/controls/AddCustomField.stories.js | 40 ++ tests/components/FinalReviewPage.test.tsx | 118 ++++++ tests/pages/communication-methods.test.jsx | 199 +++++++++- tests/pages/decision-approaches.test.jsx | 86 ++++- tests/unit/CardStack.test.jsx | 19 + .../applyFinalReviewChipEditPatch.test.ts | 42 ++ tests/unit/buildFinalReviewCategories.test.ts | 13 + tests/unit/buildPublishPayload.test.ts | 19 + tests/unit/createFlowValidation.test.ts | 34 ++ tests/unit/isCustomMethodCardId.test.ts | 18 + .../unit/mergePresetMethodsWithCustom.test.ts | 25 ++ tests/unit/methodCardSelectionOrder.test.ts | 20 + ...removeMethodCardFromFacetSelection.test.ts | 58 +++ .../stripCustomRuleSelectionFields.test.ts | 15 + 67 files changed, 3480 insertions(+), 197 deletions(-) create mode 100644 app/(app)/create/components/CustomMethodCardFieldBlocksSummary.tsx create mode 100644 app/(app)/create/components/CustomMethodCardModalBody.tsx create mode 100644 app/(app)/create/components/CustomMethodCardPresetEditPlaceholder.tsx create mode 100644 app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.container.tsx create mode 100644 app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.types.ts create mode 100644 app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.view.tsx create mode 100644 app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizardFieldBodies.view.tsx create mode 100644 app/(app)/create/components/CustomMethodCardWizard/index.tsx create mode 100644 app/(app)/create/hooks/useCustomMethodCardFieldBlocksChange.ts create mode 100644 app/components/asset/icon/image.svg create mode 100644 app/components/asset/icon/number.svg create mode 100644 app/components/asset/icon/tags.svg create mode 100644 app/components/asset/icon/text_block.svg create mode 100644 app/components/buttons/Vertical/Vertical.tsx create mode 100644 app/components/buttons/Vertical/index.tsx create mode 100644 app/components/controls/AddCustomField/AddCustomField.container.tsx create mode 100644 app/components/controls/AddCustomField/AddCustomField.types.ts create mode 100644 app/components/controls/AddCustomField/AddCustomField.view.tsx create mode 100644 app/components/controls/AddCustomField/index.tsx create mode 100644 lib/create/customMethodCardFieldBlocks.ts create mode 100644 lib/create/customMethodCardWizardConstants.ts create mode 100644 lib/create/isCustomMethodCardId.ts create mode 100644 lib/create/mergePresetMethodsWithCustom.ts create mode 100644 lib/create/methodCardSelectionOrder.ts create mode 100644 lib/create/removeMethodCardFromFacetSelection.ts create mode 100644 messages/en/create/customRule/customMethodCardWizard.json create mode 100644 stories/buttons/Vertical.stories.js create mode 100644 stories/controls/AddCustomField.stories.js create mode 100644 tests/unit/isCustomMethodCardId.test.ts create mode 100644 tests/unit/mergePresetMethodsWithCustom.test.ts create mode 100644 tests/unit/methodCardSelectionOrder.test.ts create mode 100644 tests/unit/removeMethodCardFromFacetSelection.test.ts diff --git a/app/(app)/create/components/CustomMethodCardFieldBlocksSummary.tsx b/app/(app)/create/components/CustomMethodCardFieldBlocksSummary.tsx new file mode 100644 index 0000000..83efa3f --- /dev/null +++ b/app/(app)/create/components/CustomMethodCardFieldBlocksSummary.tsx @@ -0,0 +1,279 @@ +"use client"; + +/** + * Controlled field blocks for wizard-authored method cards in Create modals + * (facet screens + final-review chip edit). When `onBlocksChange` is omitted, + * blocks render read-only (disabled controls). + * + * Layout matches preset method editors ({@link CommunicationMethodEditFields}, + * {@link DecisionApproachEditFields}): {@link ModalTextAreaField}, + * {@link ApplicableScopeField} chip rows, {@link IncrementerBlock}. + */ + +import { memo, useCallback, useRef } from "react"; +import { useMessages } from "../../../contexts/MessagesContext"; +import Chip from "../../../components/controls/Chip"; +import IncrementerBlock from "../../../components/controls/IncrementerBlock"; +import InlineTextButton from "../../../components/buttons/InlineTextButton"; +import Upload from "../../../components/controls/Upload"; +import ApplicableScopeField from "./ApplicableScopeField"; +import InputLabel from "../../../components/type/InputLabel"; +import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks"; +import ModalTextAreaField from "./ModalTextAreaField"; + +const TEXT_VALUE_MAX = 8000; + +export interface CustomMethodCardFieldBlocksSummaryProps { + blocks: CustomMethodCardFieldBlock[]; + /** When set, fields update the draft via immutable block-array replacements. */ + onBlocksChange?: (_next: CustomMethodCardFieldBlock[]) => void; +} + +function mapBlockById( + blocks: CustomMethodCardFieldBlock[], + blockId: string, + mapFn: (_b: CustomMethodCardFieldBlock) => CustomMethodCardFieldBlock, +): CustomMethodCardFieldBlock[] { + return blocks.map((b) => (b.id === blockId ? mapFn(b) : b)); +} + +function CustomMethodCardUploadBlockRow({ + block, + blocks, + patch, + uploadFileInputAriaLabel, + uploadHint, + clearFileLabel, + noFileChosen, +}: { + block: Extract; + blocks: CustomMethodCardFieldBlock[]; + patch: (_next: CustomMethodCardFieldBlock[]) => void; + uploadFileInputAriaLabel: string; + uploadHint: string; + clearFileLabel: string; + noFileChosen: string; +}) { + const uploadInputRef = useRef(null); + const displayName = block.fileName?.trim() ? block.fileName : noFileChosen; + + return ( +
+ +

+ {displayName} +

+ { + const file = e.target.files?.[0]; + const name = file?.name?.trim(); + patch( + mapBlockById(blocks, block.id, (b) => + b.kind === "upload" + ? { + ...b, + ...(name ? { fileName: name } : {}), + } + : b, + ), + ); + e.target.value = ""; + }} + /> + uploadInputRef.current?.click()} + /> + {block.fileName?.trim() ? ( + + patch( + mapBlockById(blocks, block.id, (b) => + b.kind === "upload" ? { ...b, fileName: undefined } : b, + ), + ) + } + > + {clearFileLabel} + + ) : null} +
+ ); +} + +function CustomMethodCardFieldBlocksSummaryComponent({ + blocks, + onBlocksChange, +}: CustomMethodCardFieldBlocksSummaryProps) { + const m = useMessages(); + const wiz = m.create.customRule.customMethodCardWizard; + const fm = wiz.fieldModals; + const em = wiz.editModal; + const emptyValue = em.readout.emptyValue; + const noFileChosen = em.readout.noFileChosen; + const readOnly = !onBlocksChange; + + const patch = useCallback( + (next: CustomMethodCardFieldBlock[]) => { + onBlocksChange?.(next); + }, + [onBlocksChange], + ); + + return ( +
+ {blocks.map((block) => { + if (block.kind === "text") { + return ( + + patch( + mapBlockById(blocks, block.id, (b) => + b.kind === "text" + ? { ...b, placeholderText: v.slice(0, TEXT_VALUE_MAX) } + : b, + ), + ) + } + disabled={readOnly} + /> + ); + } + + if (block.kind === "badges") { + if (readOnly) { + return ( +
+ + {block.options.length > 0 ? ( +
+ {block.options.map((opt, idx) => ( + + ))} +
+ ) : ( +

+ {emptyValue} +

+ )} +
+ ); + } + return ( + + patch( + mapBlockById(blocks, block.id, (b) => + b.kind === "badges" + ? { ...b, options: b.options.filter((o) => o !== scope) } + : b, + ), + ) + } + onAddScope={(scope) => + patch( + mapBlockById(blocks, block.id, (b) => { + if (b.kind !== "badges") return b; + if (b.options.includes(scope) || b.options.length >= 50) + return b; + return { ...b, options: [...b.options, scope] }; + }), + ) + } + /> + ); + } + + if (block.kind === "upload") { + return ( +
+ {readOnly ? ( + {}} + disabled + /> + ) : ( + + )} +
+ ); + } + + return ( + + patch( + mapBlockById(blocks, block.id, (b) => + b.kind === "proportion" ? { ...b, defaultPercent: v } : b, + ), + ) + } + formatValue={(v) => `${v}%`} + decrementAriaLabel={fm.proportion.decrementAriaLabel} + incrementAriaLabel={fm.proportion.incrementAriaLabel} + /> + ); + })} +
+ ); +} + +const CustomMethodCardFieldBlocksSummary = memo( + CustomMethodCardFieldBlocksSummaryComponent, +); +CustomMethodCardFieldBlocksSummary.displayName = + "CustomMethodCardFieldBlocksSummary"; + +export default CustomMethodCardFieldBlocksSummary; diff --git a/app/(app)/create/components/CustomMethodCardModalBody.tsx b/app/(app)/create/components/CustomMethodCardModalBody.tsx new file mode 100644 index 0000000..1974a37 --- /dev/null +++ b/app/(app)/create/components/CustomMethodCardModalBody.tsx @@ -0,0 +1,31 @@ +"use client"; + +import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks"; +import type { CreateFlowState } from "../types"; +import CustomMethodCardFieldBlocksSummary from "./CustomMethodCardFieldBlocksSummary"; +import CustomMethodCardPresetEditPlaceholder from "./CustomMethodCardPresetEditPlaceholder"; + +/** Body for Create modals when the card is user-authored (custom UUID). */ +export default function CustomMethodCardModalBody({ + cardId, + blocksById, + /** When set, used instead of `blocksById[cardId]` (e.g. final-review draft). */ + blocksOverride, + onFieldBlocksChange, +}: { + cardId: string; + blocksById: CreateFlowState["customMethodCardFieldBlocksById"]; + blocksOverride?: CustomMethodCardFieldBlock[] | null; + onFieldBlocksChange?: (_blocks: CustomMethodCardFieldBlock[]) => void; +}) { + const blocks = blocksOverride ?? blocksById?.[cardId]; + if (blocks && blocks.length > 0) { + return ( + + ); + } + return ; +} diff --git a/app/(app)/create/components/CustomMethodCardPresetEditPlaceholder.tsx b/app/(app)/create/components/CustomMethodCardPresetEditPlaceholder.tsx new file mode 100644 index 0000000..6c6cf68 --- /dev/null +++ b/app/(app)/create/components/CustomMethodCardPresetEditPlaceholder.tsx @@ -0,0 +1,26 @@ +"use client"; + +/** + * Shown in method-card Create modals and final-review chip edit when the chip + * is user-authored (`customMethodCardMetaById`) — preset section editors do + * not apply until structured parity exists with wizard field blocks. + */ + +import { memo } from "react"; +import { useMessages } from "../../../contexts/MessagesContext"; + +function CustomMethodCardPresetEditPlaceholderComponent() { + const m = useMessages(); + const body = m.create.customRule.customMethodCardWizard.editModal.placeholderBody; + + return ( +

+ {body} +

+ ); +} + +CustomMethodCardPresetEditPlaceholderComponent.displayName = + "CustomMethodCardPresetEditPlaceholder"; + +export default memo(CustomMethodCardPresetEditPlaceholderComponent); diff --git a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.container.tsx b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.container.tsx new file mode 100644 index 0000000..5f4727c --- /dev/null +++ b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.container.tsx @@ -0,0 +1,358 @@ +"use client"; + +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useMessages, useTranslation } from "../../../../contexts/MessagesContext"; +import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks"; +import { CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS } from "../../../../../lib/create/customMethodCardWizardConstants"; +import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types"; +import { CustomMethodCardWizardView } from "./CustomMethodCardWizard.view"; +import type { CustomMethodCardWizardProps } from "./CustomMethodCardWizard.types"; + +/** + * Shared 3-step add-custom-method-card flow (Figma Modal / Create — nodes + * `20066:14748`, `20094:48551`, `20066:14361`). + */ +const CustomMethodCardWizardContainer = memo( + ({ isOpen, onClose, onFinalize }) => { + const m = useMessages(); + const t = useTranslation("common"); + const w = m.create.customRule.customMethodCardWizard; + + const copy = useMemo( + () => ({ + step1: w.steps["1"], + step2: w.steps["2"], + step3: w.steps["3"], + footerFinalize: w.footer.finalize, + fieldModals: w.fieldModals, + }), + [w.fieldModals, w.footer.finalize, w.steps], + ); + + const fieldBodiesCopy = useMemo( + () => ({ + requiredHint: copy.fieldModals.requiredHint, + text: copy.fieldModals.text, + badges: copy.fieldModals.badges, + upload: copy.fieldModals.upload, + proportion: copy.fieldModals.proportion, + }), + [copy.fieldModals], + ); + + const [wizardStep, setWizardStep] = useState<1 | 2 | 3>(1); + const [policyTitle, setPolicyTitle] = useState(""); + const [policyDescription, setPolicyDescription] = useState(""); + const [addFieldExpanded, setAddFieldExpanded] = useState(false); + const [fieldTypeModal, setFieldTypeModal] = + useState(null); + const [draftFieldBlocks, setDraftFieldBlocks] = useState< + CustomMethodCardFieldBlock[] + >([]); + + const [textBlockTitle, setTextBlockTitle] = useState(""); + const [textPlaceholderBody, setTextPlaceholderBody] = useState(""); + const [badgeBlockTitle, setBadgeBlockTitle] = useState(""); + const [badgeOptions, setBadgeOptions] = useState([]); + const [uploadBlockTitle, setUploadBlockTitle] = useState(""); + const [uploadFileName, setUploadFileName] = useState( + undefined, + ); + const [proportionBlockTitle, setProportionBlockTitle] = useState(""); + const [proportionDefault, setProportionDefault] = useState(50); + + const fileInputRef = useRef(null); + + const resetFieldTypeDrafts = useCallback(() => { + setTextBlockTitle(""); + setTextPlaceholderBody(""); + setBadgeBlockTitle(""); + setBadgeOptions([]); + setUploadBlockTitle(""); + setUploadFileName(undefined); + setProportionBlockTitle(""); + setProportionDefault(50); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }, []); + + const reset = useCallback(() => { + setWizardStep(1); + setPolicyTitle(""); + setPolicyDescription(""); + setAddFieldExpanded(false); + setFieldTypeModal(null); + setDraftFieldBlocks([]); + resetFieldTypeDrafts(); + }, [resetFieldTypeDrafts]); + + useEffect(() => { + if (!isOpen) { + reset(); + } + }, [isOpen, reset]); + + const dismiss = useCallback(() => { + reset(); + onClose(); + }, [onClose, reset]); + + const titleTrim = policyTitle.trim(); + const descriptionTrim = policyDescription.trim(); + + const stepValid = useMemo(() => { + const titleOk = + titleTrim.length > 0 && + titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS; + const descriptionOk = + descriptionTrim.length > 0 && + descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS; + if (wizardStep === 1) return titleOk; + if (wizardStep === 2) return descriptionOk; + return titleOk && descriptionOk; + }, [ + descriptionTrim.length, + titleTrim.length, + wizardStep, + ]); + + const fieldModalStepValid = useMemo(() => { + if (!fieldTypeModal) return false; + if (fieldTypeModal === "text") { + const t0 = textBlockTitle.trim(); + return ( + t0.length > 0 && + t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS + ); + } + if (fieldTypeModal === "badges") { + const t0 = badgeBlockTitle.trim(); + return ( + t0.length > 0 && + t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS + ); + } + if (fieldTypeModal === "upload") { + const t0 = uploadBlockTitle.trim(); + return ( + t0.length > 0 && + t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS + ); + } + const t0 = proportionBlockTitle.trim(); + return ( + t0.length > 0 && + t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS && + proportionDefault >= 1 && + proportionDefault <= 100 + ); + }, [ + badgeBlockTitle, + fieldTypeModal, + proportionBlockTitle, + proportionDefault, + textBlockTitle, + uploadBlockTitle, + ]); + + const headerTitle = + wizardStep === 1 + ? copy.step1.title + : wizardStep === 2 + ? copy.step2.title + : copy.step3.title; + + const headerDescription = + wizardStep === 1 + ? copy.step1.description + : wizardStep === 2 + ? copy.step2.description + : copy.step3.description; + + const fieldModalHeader = fieldTypeModal + ? copy.fieldModals[fieldTypeModal] + : null; + + const shellTitle = fieldModalHeader?.title ?? headerTitle; + const shellDescription = fieldModalHeader?.description ?? headerDescription; + + const nextLabel = fieldTypeModal + ? copy.fieldModals.addField + : wizardStep === 3 + ? copy.footerFinalize + : t("buttons.next"); + + const shellNextDisabled = fieldTypeModal + ? !fieldModalStepValid + : !stepValid; + + const handleShellClose = useCallback(() => { + if (fieldTypeModal) { + setFieldTypeModal(null); + return; + } + dismiss(); + }, [dismiss, fieldTypeModal]); + + const handleBack = useCallback(() => { + if (fieldTypeModal) { + setFieldTypeModal(null); + return; + } + if (wizardStep === 1) { + dismiss(); + return; + } + setWizardStep((s) => (s === 2 ? 1 : 2)); + }, [dismiss, fieldTypeModal, wizardStep]); + + const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => { + resetFieldTypeDrafts(); + setFieldTypeModal(ft); + }, [resetFieldTypeDrafts]); + + const handleFileChosen = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + setUploadFileName(file?.name); + }, + [], + ); + + const handleBadgeAddOption = useCallback((label: string) => { + setBadgeOptions((prev) => + prev.includes(label) ? prev : [...prev, label], + ); + }, []); + + const appendFieldBlock = useCallback(() => { + if (!fieldTypeModal || !fieldModalStepValid) return; + const id = crypto.randomUUID(); + let block: CustomMethodCardFieldBlock; + switch (fieldTypeModal) { + case "text": + block = { + kind: "text", + id, + blockTitle: textBlockTitle.trim(), + placeholderText: textPlaceholderBody, + }; + break; + case "badges": + block = { + kind: "badges", + id, + blockTitle: badgeBlockTitle.trim(), + options: [...badgeOptions], + }; + break; + case "upload": + block = { + kind: "upload", + id, + blockTitle: uploadBlockTitle.trim(), + fileName: uploadFileName, + }; + break; + default: + block = { + kind: "proportion", + id, + blockTitle: proportionBlockTitle.trim(), + defaultPercent: proportionDefault, + }; + } + setDraftFieldBlocks((prev) => [...prev, block]); + setFieldTypeModal(null); + }, [ + badgeBlockTitle, + badgeOptions, + fieldModalStepValid, + fieldTypeModal, + proportionBlockTitle, + proportionDefault, + textBlockTitle, + textPlaceholderBody, + uploadBlockTitle, + uploadFileName, + ]); + + const handleNext = useCallback(() => { + if (fieldTypeModal) { + appendFieldBlock(); + return; + } + if (!stepValid) return; + if (wizardStep === 3) { + onFinalize({ + title: titleTrim, + description: descriptionTrim, + fieldBlocks: draftFieldBlocks, + }); + dismiss(); + return; + } + setWizardStep((s) => (s === 1 ? 2 : 3)); + }, [ + appendFieldBlock, + descriptionTrim, + dismiss, + draftFieldBlocks, + fieldTypeModal, + onFinalize, + stepValid, + titleTrim, + wizardStep, + ]); + + return ( + setAddFieldExpanded(true)} + onSelectFieldType={handleSelectFieldType} + fieldTypeModal={fieldTypeModal} + fieldBodiesCopy={fieldBodiesCopy} + fieldBodiesProps={{ + textBlockTitle, + textPlaceholderBody, + onTextBlockTitleChange: setTextBlockTitle, + onTextPlaceholderBodyChange: setTextPlaceholderBody, + badgeBlockTitle, + badgeOptions, + onBadgeBlockTitleChange: setBadgeBlockTitle, + onBadgeAddOption: handleBadgeAddOption, + uploadBlockTitle, + onUploadBlockTitleChange: setUploadBlockTitle, + fileInputRef, + onFileChosen: handleFileChosen, + proportionBlockTitle, + proportionDefault, + onProportionBlockTitleChange: setProportionBlockTitle, + onProportionDefaultChange: setProportionDefault, + }} + nextDisabled={shellNextDisabled} + nextLabel={nextLabel} + showBackButton + onBack={handleBack} + onNext={handleNext} + stepper={!fieldTypeModal} + /> + ); + }, +); + +CustomMethodCardWizardContainer.displayName = "CustomMethodCardWizard"; + +export default CustomMethodCardWizardContainer; diff --git a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.types.ts b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.types.ts new file mode 100644 index 0000000..4c99ef0 --- /dev/null +++ b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.types.ts @@ -0,0 +1,120 @@ +import type { RefObject } from "react"; +import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types"; +import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks"; + +export interface CustomMethodCardWizardFieldBodiesCopy { + requiredHint: string; + text: { + blockTitleLabel: string; + blockTitlePlaceholder: string; + placeholderLabel: string; + placeholderFieldPlaceholder: string; + }; + badges: { + blockTitleLabel: string; + blockTitlePlaceholder: string; + optionsLabel: string; + addOptionLabel: string; + }; + upload: { + blockTitleLabel: string; + blockTitlePlaceholder: string; + uploadFileInputAriaLabel: string; + uploadHint: string; + }; + proportion: { + blockTitleLabel: string; + blockTitlePlaceholder: string; + defaultLabel: string; + decrementAriaLabel: string; + incrementAriaLabel: string; + }; +} + +export interface CustomMethodCardWizardCopy { + step1: { title: string; description: string; fieldPlaceholder: string }; + step2: { title: string; description: string; fieldPlaceholder: string }; + step3: { title: string; description: string }; + footerFinalize: string; + fieldModals: { + addField: string; + requiredHint: string; + text: CustomMethodCardWizardFieldBodiesCopy["text"] & { + title: string; + description: string; + }; + badges: CustomMethodCardWizardFieldBodiesCopy["badges"] & { + title: string; + description: string; + }; + upload: CustomMethodCardWizardFieldBodiesCopy["upload"] & { + title: string; + description: string; + }; + proportion: CustomMethodCardWizardFieldBodiesCopy["proportion"] & { + title: string; + description: string; + }; + }; +} + +export interface CustomMethodCardWizardProps { + isOpen: boolean; + onClose: () => void; + /** Called when the user completes step 3; parent assigns id and persists state. */ + onFinalize: (payload: { + title: string; + description: string; + fieldBlocks: CustomMethodCardFieldBlock[]; + }) => void; +} + +export interface CustomMethodCardWizardFieldBodiesViewProps { + fieldType: AddCustomFieldType; + copy: CustomMethodCardWizardFieldBodiesCopy; + textBlockTitle: string; + textPlaceholderBody: string; + onTextBlockTitleChange: (_v: string) => void; + onTextPlaceholderBodyChange: (_v: string) => void; + badgeBlockTitle: string; + badgeOptions: string[]; + onBadgeBlockTitleChange: (_v: string) => void; + onBadgeAddOption: (_v: string) => void; + uploadBlockTitle: string; + onUploadBlockTitleChange: (_v: string) => void; + fileInputRef: RefObject; + onFileChosen: (e: React.ChangeEvent) => void; + proportionBlockTitle: string; + proportionDefault: number; + onProportionBlockTitleChange: (_v: string) => void; + onProportionDefaultChange: (_v: number) => void; +} + +export interface CustomMethodCardWizardViewProps { + isOpen: boolean; + onDismiss: () => void; + wizardStep: 1 | 2 | 3; + title: string; + description: string; + policyTitle: string; + policyDescription: string; + addFieldExpanded: boolean; + copy: CustomMethodCardWizardCopy; + maxChars: number; + onPolicyTitleChange: (v: string) => void; + onPolicyDescriptionChange: (v: string) => void; + onPressAddCustomField: () => void; + onSelectFieldType: (t: AddCustomFieldType) => void; + fieldTypeModal: AddCustomFieldType | null; + fieldBodiesCopy: CustomMethodCardWizardFieldBodiesCopy; + fieldBodiesProps: Omit< + CustomMethodCardWizardFieldBodiesViewProps, + "fieldType" | "copy" + >; + nextDisabled: boolean; + nextLabel: string; + showBackButton: boolean; + onBack: () => void; + onNext: () => void; + stepper: boolean; +} diff --git a/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.view.tsx b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.view.tsx new file mode 100644 index 0000000..b51ee12 --- /dev/null +++ b/app/(app)/create/components/CustomMethodCardWizard/CustomMethodCardWizard.view.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { memo } from "react"; +import Create from "../../../../components/modals/Create"; +import InputWithCounter from "../../../../components/controls/InputWithCounter"; +import TextArea from "../../../../components/controls/TextArea"; +import AddCustomField from "../../../../components/controls/AddCustomField"; +import { CustomMethodCardWizardFieldBodiesView } from "./CustomMethodCardWizardFieldBodies.view"; +import type { CustomMethodCardWizardViewProps } from "./CustomMethodCardWizard.types"; + +function CustomMethodCardWizardViewComponent({ + isOpen, + onDismiss, + wizardStep, + title, + description, + policyTitle, + policyDescription, + addFieldExpanded, + copy, + maxChars, + onPolicyTitleChange, + onPolicyDescriptionChange, + onPressAddCustomField, + onSelectFieldType, + fieldTypeModal, + fieldBodiesCopy, + fieldBodiesProps, + nextDisabled, + nextLabel, + showBackButton, + onBack, + onNext, + stepper, +}: CustomMethodCardWizardViewProps) { + return ( + + {fieldTypeModal ? ( + + ) : null} + {!fieldTypeModal && wizardStep === 1 ? ( + + ) : null} + {!fieldTypeModal && wizardStep === 2 ? ( +