From 753220f97b3325baff0af41a8e36a1347b6ee4bf Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Fri, 22 May 2026 13:30:47 -0600 Subject: [PATCH] Cleanup pass 2 --- AGENTS.md | 1 + .../components/FinalReviewChipEditModal.tsx | 62 ++-- app/(app)/create/hooks/useCreateFlowExit.ts | 9 +- .../hooks/useDiscardCustomizeConfirm.tsx | 78 +++++ .../card/CommunicationMethodsScreen.tsx | 35 +- .../screens/card/ConflictManagementScreen.tsx | 35 +- .../screens/card/MembershipMethodsScreen.tsx | 35 +- .../right-rail/DecisionApproachesScreen.tsx | 35 +- .../screens/select/CoreValuesSelectScreen.tsx | 48 +-- app/components/cards/Rule/Rule.container.tsx | 6 + app/components/cards/Rule/Rule.types.ts | 3 + app/components/cards/Rule/Rule.view.tsx | 8 +- .../LanguageSwitcher.container.tsx | 22 -- .../LanguageSwitcher.types.ts | 9 - .../LanguageSwitcher.view.tsx | 42 --- .../localization/LanguageSwitcher/index.tsx | 2 - .../AskOrganizerInquiryModal.container.tsx | 20 +- .../AskOrganizerInquiryModal.types.ts | 32 ++ .../AskOrganizerInquiryModal.view.tsx | 46 +-- app/components/modals/Login/LoginForm.tsx | 4 +- .../ModalFooter/ModalFooter.container.tsx | 13 +- .../modals/ModalFooter/ModalFooter.view.tsx | 17 +- .../CreateFlowTopNav.container.tsx | 10 + .../CreateFlowTopNav.types.ts | 10 + .../CreateFlowTopNav.view.tsx | 37 ++- app/components/navigation/Footer.tsx | 6 +- .../AskOrganizer/AskOrganizer.container.tsx | 56 ++-- .../AskOrganizer/AskOrganizer.types.ts | 9 +- .../AskOrganizer/AskOrganizer.view.tsx | 12 +- .../ContentBanner/ContentBanner.container.tsx | 10 + .../ContentBanner/ContentBanner.types.ts | 2 + .../ContentBanner/ContentBanner.view.tsx | 9 +- .../RuleStack/RuleStack.container.tsx | 7 +- .../sections/RuleStack/RuleStack.types.ts | 4 +- .../sections/RuleStack/RuleStack.view.tsx | 14 +- app/hooks/index.ts | 5 +- app/hooks/useAsyncConfirm.tsx | 76 +++++ app/layout.tsx | 21 +- docs/guides/ops-backend-deploy.md | 8 +- docs/guides/static-assets.md | 10 + docs/testing-guide.md | 1 + knip.json | 25 +- lib/assetUtils.ts | 2 - lib/create/methodCardCustomizeSession.ts | 8 +- messages/en/components/featureGrid.json | 2 +- messages/en/components/footer.json | 6 +- .../en/create/customRule/modalKebabMenu.json | 6 +- package.json | 2 +- public/assets/icons/icon-alert.svg | 3 - public/assets/icons/icon-pointer.svg | 3 - public/assets/share/discord.svg | 3 + public/assets/share/link.svg | 4 + public/assets/share/mail.svg | 8 + public/assets/share/signal.svg | 3 + public/assets/share/slack.svg | 6 + ...operational-security-mutual-aid-banner.svg | 9 - .../resolving-active-conflicts-banner.svg | 9 - stories/asset/Shapes.stories.js | 18 + stories/controls/SelectOption.stories.js | 36 ++ .../localization/LanguageSwitcher.stories.js | 19 -- stories/modals/AskOrganizerInquiry.stories.js | 33 ++ stories/sections/AskOrganizer.stories.js | 16 - stories/sections/Book.stories.js | 22 ++ stories/type/TripleTextBlock.stories.js | 14 + tests/components/AskOrganizer.test.tsx | 7 +- ...unicationMethodsScreenPersistence.test.tsx | 61 ++-- tests/components/FinalReviewPage.test.tsx | 22 +- tests/components/LanguageSwitcher.test.tsx | 23 -- tests/lib/methodCardCustomizeSession.test.ts | 22 +- .../unit/MarkdownProcessing.test.js.disabled | 199 ----------- tests/unit/authMagicLinkRequestRoute.test.ts | 131 ++++++++ tests/unit/authSessionRoute.test.ts | 71 ++++ tests/unit/content.test.js.disabled | 310 ------------------ .../rulesStakeholderMutationsRoute.test.ts | 222 +++++++++++++ tests/unit/uploadsByIdRoute.test.ts | 62 ++++ tests/unit/uploadsPostRoute.test.ts | 102 ++++++ 76 files changed, 1338 insertions(+), 1020 deletions(-) create mode 100644 app/(app)/create/hooks/useDiscardCustomizeConfirm.tsx delete mode 100644 app/components/localization/LanguageSwitcher/LanguageSwitcher.container.tsx delete mode 100644 app/components/localization/LanguageSwitcher/LanguageSwitcher.types.ts delete mode 100644 app/components/localization/LanguageSwitcher/LanguageSwitcher.view.tsx delete mode 100644 app/components/localization/LanguageSwitcher/index.tsx create mode 100644 app/hooks/useAsyncConfirm.tsx delete mode 100644 public/assets/icons/icon-alert.svg delete mode 100644 public/assets/icons/icon-pointer.svg create mode 100644 public/assets/share/discord.svg create mode 100644 public/assets/share/link.svg create mode 100644 public/assets/share/mail.svg create mode 100644 public/assets/share/signal.svg create mode 100644 public/assets/share/slack.svg delete mode 100644 public/content/blog/operational-security-mutual-aid-banner.svg delete mode 100644 public/content/blog/resolving-active-conflicts-banner.svg create mode 100644 stories/asset/Shapes.stories.js create mode 100644 stories/controls/SelectOption.stories.js delete mode 100644 stories/localization/LanguageSwitcher.stories.js create mode 100644 stories/modals/AskOrganizerInquiry.stories.js create mode 100644 stories/sections/Book.stories.js create mode 100644 stories/type/TripleTextBlock.stories.js delete mode 100644 tests/components/LanguageSwitcher.test.tsx delete mode 100644 tests/unit/MarkdownProcessing.test.js.disabled create mode 100644 tests/unit/authMagicLinkRequestRoute.test.ts create mode 100644 tests/unit/authSessionRoute.test.ts delete mode 100644 tests/unit/content.test.js.disabled create mode 100644 tests/unit/rulesStakeholderMutationsRoute.test.ts create mode 100644 tests/unit/uploadsByIdRoute.test.ts create mode 100644 tests/unit/uploadsPostRoute.test.ts diff --git a/AGENTS.md b/AGENTS.md index 4583413..766871d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,6 +68,7 @@ Run these (in order) before declaring a change done: ```bash rm -rf .next # only if you moved/renamed routes or layouts npx tsc --noEmit # type check +npm run knip # unused files / exports (local; no remote CI) npx vitest run # unit + component (~185 test files) npx next build # production build + route manifest ``` diff --git a/app/(app)/create/components/FinalReviewChipEditModal.tsx b/app/(app)/create/components/FinalReviewChipEditModal.tsx index 3dcb11d..b06c9f7 100644 --- a/app/(app)/create/components/FinalReviewChipEditModal.tsx +++ b/app/(app)/create/components/FinalReviewChipEditModal.tsx @@ -28,6 +28,7 @@ import { import CustomMethodCardModalBody from "./CustomMethodCardModalBody"; import MethodCardCustomizeModalHeader from "./MethodCardCustomizeModalHeader"; import { buildCustomRuleModalKebabMenu } from "./customRuleModalKebabMenu"; +import { useDiscardCustomizeConfirm } from "../hooks/useDiscardCustomizeConfirm"; import { communicationPresetFor, conflictManagementPresetFor, @@ -52,7 +53,6 @@ import { } from "../../../../lib/create/coreValueChipFacet"; import { captureMethodCardCustomizeSnapshot, - confirmDiscardMethodCardCustomizeSession, isMethodCardCustomizeSessionDirty, type MethodCardCustomizeSnapshot, type MethodCardHeaderDraft, @@ -171,6 +171,8 @@ export function FinalReviewChipEditModal({ const tModal = useTranslation( "create.reviewAndComplete.finalReview.chipEditModal", ); + const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } = + useDiscardCustomizeConfirm(); const [draft, setDraft] = useState(null); const [modalEditUnlocked, setModalEditUnlocked] = useState(false); @@ -342,32 +344,30 @@ export function FinalReviewChipEditModal({ onClose(); }, [onClose]); - const handleModalClose = useCallback(() => { + const handleModalClose = useCallback(async () => { if ( target && target.groupKey === "coreValues" && - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, coreCustomizeSnapshotRef.current, draft?.groupKey === "coreValues" ? draft.value : null, null, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } if ( target && isMethodFacetGroup(target.groupKey) && - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, customizeSnapshotRef.current, methodDetailDraftForCustomizeSession(draft), draftFieldBlocks, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -380,17 +380,17 @@ export function FinalReviewChipEditModal({ } finalizeModalClose(); }, [ + confirmDiscard, customizeHeaderDraft, draft, draftFieldBlocks, finalizeModalClose, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, replaceState, target, ]); - const handleCancelCustomize = useCallback(() => { + const handleCancelCustomize = useCallback(async () => { if (!modalEditUnlocked || !target) { return; } @@ -404,13 +404,12 @@ export function FinalReviewChipEditModal({ } if ( draft?.groupKey === "coreValues" && - isMethodCardCustomizeSessionDirty( + !(await confirmDirtyCustomizeCancel( snap, draft.value, null, customizeHeaderDraft, - ) && - !window.confirm(modalKebabMenu.discardUnsavedCustomizeChanges) + )) ) { return; } @@ -435,13 +434,12 @@ export function FinalReviewChipEditModal({ return; } if ( - isMethodCardCustomizeSessionDirty( + !(await confirmDirtyCustomizeCancel( snap, methodDetailDraftForCustomizeSession(draft), draftFieldBlocks, customizeHeaderDraft, - ) && - !window.confirm(modalKebabMenu.discardUnsavedCustomizeChanges) + )) ) { return; } @@ -451,11 +449,11 @@ export function FinalReviewChipEditModal({ customizeSnapshotRef.current = null; setCustomizeHeaderDraft(null); }, [ + confirmDirtyCustomizeCancel, customizeHeaderDraft, draft, draftFieldBlocks, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, target, ]); @@ -565,7 +563,7 @@ export function FinalReviewChipEditModal({ tCm, ]); - const handleRemoveSelectedFromModal = useCallback(() => { + const handleRemoveSelectedFromModal = useCallback(async () => { if (!target || !isMethodFacetGroup(target.groupKey)) { return; } @@ -575,14 +573,13 @@ export function FinalReviewChipEditModal({ } onInteract?.(); if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, customizeSnapshotRef.current, methodDetailDraftForCustomizeSession(draft), draftFieldBlocks, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -597,32 +594,31 @@ export function FinalReviewChipEditModal({ })); finalizeModalClose(); }, [ + confirmDiscard, customizeHeaderDraft, draft, draftFieldBlocks, finalizeModalClose, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, onInteract, replaceState, selectionIdsForTarget, target, ]); - const handleRemoveCoreValueFromModal = useCallback(() => { + const handleRemoveCoreValueFromModal = useCallback(async () => { if (!target || target.groupKey !== "coreValues") { return; } onInteract?.(); if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, coreCustomizeSnapshotRef.current, draft?.groupKey === "coreValues" ? draft.value : null, null, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -634,17 +630,17 @@ export function FinalReviewChipEditModal({ })); finalizeModalClose(); }, [ + confirmDiscard, customizeHeaderDraft, draft, finalizeModalClose, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, onInteract, replaceState, target, ]); - const handleDuplicateCoreValue = useCallback(() => { + const handleDuplicateCoreValue = useCallback(async () => { if ( !target || target.groupKey !== "coreValues" || @@ -659,14 +655,13 @@ export function FinalReviewChipEditModal({ return; } if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, coreCustomizeSnapshotRef.current, draft.value, null, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -711,10 +706,10 @@ export function FinalReviewChipEditModal({ chipLabel: outcome.newLabel, }); }, [ + confirmDiscard, customizeHeaderDraft, draft, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, modalKebabMenu.duplicateTitleSuffix, onEditTargetChange, onInteract, @@ -1015,6 +1010,7 @@ export function FinalReviewChipEditModal({ : showMethodModalPrimary; return ( + <> + {confirmDialog} + ); } diff --git a/app/(app)/create/hooks/useCreateFlowExit.ts b/app/(app)/create/hooks/useCreateFlowExit.ts index 7078c80..0030c39 100644 --- a/app/(app)/create/hooks/useCreateFlowExit.ts +++ b/app/(app)/create/hooks/useCreateFlowExit.ts @@ -31,7 +31,7 @@ export function useCreateFlowExit({ user: { id: string; email: string } | null; /** When save fails, surface the server message in the create shell banner (no leave confirm). */ setDraftSaveBannerMessage?: (_message: string | null) => void; - /** When exit would discard unsaved work, return true to proceed. Defaults to `window.confirm`. */ + /** When exit would discard unsaved work, return true to proceed. Defaults to denying leave. */ confirmLeave?: () => Promise; }): (_options?: { saveDraft?: boolean }) => Promise { return useCallback( @@ -41,12 +41,7 @@ export function useCreateFlowExit({ const saveDraft = options?.saveDraft ?? false; if (!saveDraft) { - const confirmFn = - confirmLeave ?? - (async () => { - if (typeof window === "undefined") return true; - return window.confirm(messages.create.topNav.leaveConfirmLoss); - }); + const confirmFn = confirmLeave ?? (async () => false); const confirmed = await confirmFn(); if (!confirmed) return; } diff --git a/app/(app)/create/hooks/useDiscardCustomizeConfirm.tsx b/app/(app)/create/hooks/useDiscardCustomizeConfirm.tsx new file mode 100644 index 0000000..a30bc3a --- /dev/null +++ b/app/(app)/create/hooks/useDiscardCustomizeConfirm.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useCallback } from "react"; +import messages from "../../../../messages/en/index"; +import { useAsyncConfirm } from "../../../hooks/useAsyncConfirm"; +import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks"; +import { + confirmDiscardMethodCardCustomizeSession, + isMethodCardCustomizeSessionDirty, + type MethodCardCustomizeSnapshot, + type MethodCardHeaderDraft, +} from "../../../../lib/create/methodCardCustomizeSession"; + +const copy = messages.create.customRule.modalKebabMenu; + +const confirmOptions = { + title: copy.discardUnsavedCustomizeChangesTitle, + description: copy.discardUnsavedCustomizeChangesDescription, + proceedText: copy.discardUnsavedCustomizeChangesProceed, + cancelText: copy.discardUnsavedCustomizeChangesCancel, +}; + +/** + * Create-flow confirm for exiting customize mode with unsaved edits. + * + * @returns Async helpers plus `confirmDialog` to render once in the screen JSX. + */ +export function useDiscardCustomizeConfirm() { + const { requestConfirm, confirmDialog } = useAsyncConfirm(); + + const runConfirm = useCallback( + () => requestConfirm(confirmOptions), + [requestConfirm], + ); + + const confirmDiscard = useCallback( + async ( + modalEditUnlocked: boolean, + snapshot: MethodCardCustomizeSnapshot | null, + pendingDraft: TDraft | null, + draftFieldBlocks: CustomMethodCardFieldBlock[] | null, + headerDraft: MethodCardHeaderDraft | null, + ) => + confirmDiscardMethodCardCustomizeSession( + modalEditUnlocked, + snapshot, + pendingDraft, + draftFieldBlocks, + headerDraft, + runConfirm, + ), + [runConfirm], + ); + + const confirmDirtyCustomizeCancel = useCallback( + async ( + snapshot: MethodCardCustomizeSnapshot, + pendingDraft: TDraft | null, + draftFieldBlocks: CustomMethodCardFieldBlock[] | null, + headerDraft: MethodCardHeaderDraft | null, + ) => { + if ( + !isMethodCardCustomizeSessionDirty( + snapshot, + pendingDraft, + draftFieldBlocks, + headerDraft, + ) + ) { + return true; + } + return runConfirm(); + }, + [runConfirm], + ); + + return { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog }; +} diff --git a/app/(app)/create/screens/card/CommunicationMethodsScreen.tsx b/app/(app)/create/screens/card/CommunicationMethodsScreen.tsx index ee8bacf..1cd86a1 100644 --- a/app/(app)/create/screens/card/CommunicationMethodsScreen.tsx +++ b/app/(app)/create/screens/card/CommunicationMethodsScreen.tsx @@ -19,6 +19,7 @@ import { useState, useCallback, useMemo, useRef } from "react"; import { useMessages } from "../../../../contexts/MessagesContext"; import { useCreateFlow } from "../../context/CreateFlowContext"; import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp"; +import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm"; import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering"; import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; import CardStack from "../../../../components/cards/CardStack"; @@ -53,8 +54,6 @@ import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalK import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch"; import { captureMethodCardCustomizeSnapshot, - confirmDiscardMethodCardCustomizeSession, - isMethodCardCustomizeSessionDirty, type MethodCardCustomizeSnapshot, type MethodCardHeaderDraft, } from "../../../../../lib/create/methodCardCustomizeSession"; @@ -65,6 +64,8 @@ export function CommunicationMethodsScreen() { const comm = m.create.customRule.communication; const modalKebabMenu = m.create.customRule.modalKebabMenu; const mdUp = useCreateFlowMdUp(); + const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } = + useDiscardCustomizeConfirm(); const { state, updateState, replaceState, markCreateFlowInteraction } = useCreateFlow(); const pendingEphemeralDuplicateIdRef = useRef(null); @@ -201,16 +202,15 @@ export function CommunicationMethodsScreen() { ], ); - const handleCreateModalClose = useCallback(() => { + const handleCreateModalClose = useCallback(async () => { if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, customizeSnapshotRef.current, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -241,15 +241,15 @@ export function CommunicationMethodsScreen() { setDraftFieldBlocks(null); setCustomizeHeaderDraft(null); }, [ + confirmDiscard, customizeHeaderDraft, draftFieldBlocks, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, replaceState, ]); - const handleCancelCustomize = useCallback(() => { + const handleCancelCustomize = useCallback(async () => { if (!modalEditUnlocked) { return; } @@ -262,13 +262,12 @@ export function CommunicationMethodsScreen() { return; } if ( - isMethodCardCustomizeSessionDirty( + !(await confirmDirtyCustomizeCancel( snap, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - ) && - !window.confirm(modalKebabMenu.discardUnsavedCustomizeChanges) + )) ) { return; } @@ -278,27 +277,26 @@ export function CommunicationMethodsScreen() { customizeSnapshotRef.current = null; setCustomizeHeaderDraft(null); }, [ + confirmDirtyCustomizeCancel, customizeHeaderDraft, draftFieldBlocks, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, ]); - const handleRemoveSelectedFromModal = useCallback(() => { + const handleRemoveSelectedFromModal = useCallback(async () => { if (!pendingCardId || !selectedIds.includes(pendingCardId)) { return; } markCreateFlowInteraction(); if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, customizeSnapshotRef.current, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -310,14 +308,14 @@ export function CommunicationMethodsScreen() { pendingCardId, ), ); - handleCreateModalClose(); + await handleCreateModalClose(); }, [ + confirmDiscard, customizeHeaderDraft, draftFieldBlocks, handleCreateModalClose, markCreateFlowInteraction, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, pendingCardId, selectedIds, @@ -829,6 +827,7 @@ export function CommunicationMethodsScreen() { uploadCreateFlowFile(file, "customMethodAttachment") } /> + {confirmDialog} ); } diff --git a/app/(app)/create/screens/card/ConflictManagementScreen.tsx b/app/(app)/create/screens/card/ConflictManagementScreen.tsx index 72ca3bd..30d2ab4 100644 --- a/app/(app)/create/screens/card/ConflictManagementScreen.tsx +++ b/app/(app)/create/screens/card/ConflictManagementScreen.tsx @@ -16,6 +16,7 @@ import { useState, useCallback, useMemo, useRef } from "react"; import { useMessages } from "../../../../contexts/MessagesContext"; import { useCreateFlow } from "../../context/CreateFlowContext"; import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp"; +import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm"; import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering"; import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; import CardStack from "../../../../components/cards/CardStack"; @@ -50,8 +51,6 @@ import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalK import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch"; import { captureMethodCardCustomizeSnapshot, - confirmDiscardMethodCardCustomizeSession, - isMethodCardCustomizeSessionDirty, type MethodCardCustomizeSnapshot, type MethodCardHeaderDraft, } from "../../../../../lib/create/methodCardCustomizeSession"; @@ -62,6 +61,8 @@ export function ConflictManagementScreen() { const cm = m.create.customRule.conflictManagement; const modalKebabMenu = m.create.customRule.modalKebabMenu; const mdUp = useCreateFlowMdUp(); + const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } = + useDiscardCustomizeConfirm(); const { state, updateState, replaceState, markCreateFlowInteraction } = useCreateFlow(); const pendingEphemeralDuplicateIdRef = useRef(null); @@ -202,16 +203,15 @@ export function ConflictManagementScreen() { ], ); - const handleCreateModalClose = useCallback(() => { + const handleCreateModalClose = useCallback(async () => { if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, customizeSnapshotRef.current, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -242,15 +242,15 @@ export function ConflictManagementScreen() { setDraftFieldBlocks(null); setCustomizeHeaderDraft(null); }, [ + confirmDiscard, customizeHeaderDraft, draftFieldBlocks, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, replaceState, ]); - const handleCancelCustomize = useCallback(() => { + const handleCancelCustomize = useCallback(async () => { if (!modalEditUnlocked) { return; } @@ -263,13 +263,12 @@ export function ConflictManagementScreen() { return; } if ( - isMethodCardCustomizeSessionDirty( + !(await confirmDirtyCustomizeCancel( snap, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - ) && - !window.confirm(modalKebabMenu.discardUnsavedCustomizeChanges) + )) ) { return; } @@ -279,27 +278,26 @@ export function ConflictManagementScreen() { customizeSnapshotRef.current = null; setCustomizeHeaderDraft(null); }, [ + confirmDirtyCustomizeCancel, customizeHeaderDraft, draftFieldBlocks, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, ]); - const handleRemoveSelectedFromModal = useCallback(() => { + const handleRemoveSelectedFromModal = useCallback(async () => { if (!pendingCardId || !selectedIds.includes(pendingCardId)) { return; } markCreateFlowInteraction(); if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, customizeSnapshotRef.current, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -311,14 +309,14 @@ export function ConflictManagementScreen() { pendingCardId, ), ); - handleCreateModalClose(); + await handleCreateModalClose(); }, [ + confirmDiscard, customizeHeaderDraft, draftFieldBlocks, handleCreateModalClose, markCreateFlowInteraction, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, pendingCardId, selectedIds, @@ -828,6 +826,7 @@ export function ConflictManagementScreen() { uploadCreateFlowFile(file, "customMethodAttachment") } /> + {confirmDialog} ); } diff --git a/app/(app)/create/screens/card/MembershipMethodsScreen.tsx b/app/(app)/create/screens/card/MembershipMethodsScreen.tsx index 55d96dc..1afe6e3 100644 --- a/app/(app)/create/screens/card/MembershipMethodsScreen.tsx +++ b/app/(app)/create/screens/card/MembershipMethodsScreen.tsx @@ -17,6 +17,7 @@ import { useState, useCallback, useMemo, useRef } from "react"; import { useMessages } from "../../../../contexts/MessagesContext"; import { useCreateFlow } from "../../context/CreateFlowContext"; import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp"; +import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm"; import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering"; import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; import CardStack from "../../../../components/cards/CardStack"; @@ -51,8 +52,6 @@ import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalK import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch"; import { captureMethodCardCustomizeSnapshot, - confirmDiscardMethodCardCustomizeSession, - isMethodCardCustomizeSessionDirty, type MethodCardCustomizeSnapshot, type MethodCardHeaderDraft, } from "../../../../../lib/create/methodCardCustomizeSession"; @@ -63,6 +62,8 @@ export function MembershipMethodsScreen() { const mem = m.create.customRule.membership; const modalKebabMenu = m.create.customRule.modalKebabMenu; const mdUp = useCreateFlowMdUp(); + const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } = + useDiscardCustomizeConfirm(); const { state, updateState, replaceState, markCreateFlowInteraction } = useCreateFlow(); const pendingEphemeralDuplicateIdRef = useRef(null); @@ -199,16 +200,15 @@ export function MembershipMethodsScreen() { ], ); - const handleCreateModalClose = useCallback(() => { + const handleCreateModalClose = useCallback(async () => { if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, customizeSnapshotRef.current, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -239,15 +239,15 @@ export function MembershipMethodsScreen() { setDraftFieldBlocks(null); setCustomizeHeaderDraft(null); }, [ + confirmDiscard, customizeHeaderDraft, draftFieldBlocks, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, replaceState, ]); - const handleCancelCustomize = useCallback(() => { + const handleCancelCustomize = useCallback(async () => { if (!modalEditUnlocked) { return; } @@ -260,13 +260,12 @@ export function MembershipMethodsScreen() { return; } if ( - isMethodCardCustomizeSessionDirty( + !(await confirmDirtyCustomizeCancel( snap, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - ) && - !window.confirm(modalKebabMenu.discardUnsavedCustomizeChanges) + )) ) { return; } @@ -276,27 +275,26 @@ export function MembershipMethodsScreen() { customizeSnapshotRef.current = null; setCustomizeHeaderDraft(null); }, [ + confirmDirtyCustomizeCancel, customizeHeaderDraft, draftFieldBlocks, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, ]); - const handleRemoveSelectedFromModal = useCallback(() => { + const handleRemoveSelectedFromModal = useCallback(async () => { if (!pendingCardId || !selectedIds.includes(pendingCardId)) { return; } markCreateFlowInteraction(); if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, customizeSnapshotRef.current, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -304,14 +302,14 @@ export function MembershipMethodsScreen() { updateState( removeMethodCardFromFacetSelection(state, "membership", pendingCardId), ); - handleCreateModalClose(); + await handleCreateModalClose(); }, [ + confirmDiscard, customizeHeaderDraft, draftFieldBlocks, handleCreateModalClose, markCreateFlowInteraction, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, pendingCardId, selectedIds, @@ -821,6 +819,7 @@ export function MembershipMethodsScreen() { uploadCreateFlowFile(file, "customMethodAttachment") } /> + {confirmDialog} ); } diff --git a/app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx b/app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx index 57d0362..86db7b7 100644 --- a/app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx +++ b/app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx @@ -26,6 +26,7 @@ import type { InfoMessageBoxItem } from "../../../../components/controls/InfoMes import { useMessages } from "../../../../contexts/MessagesContext"; import { useCreateFlow } from "../../context/CreateFlowContext"; import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp"; +import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm"; import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering"; import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell"; import { DecisionApproachEditFields } from "../../components/methodEditFields"; @@ -52,8 +53,6 @@ import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalK import { methodCardMetaWithCustomizeHeader } from "../../../../../lib/create/methodCardCustomizeMetaPatch"; import { captureMethodCardCustomizeSnapshot, - confirmDiscardMethodCardCustomizeSession, - isMethodCardCustomizeSessionDirty, type MethodCardCustomizeSnapshot, type MethodCardHeaderDraft, } from "../../../../../lib/create/methodCardCustomizeSession"; @@ -64,6 +63,8 @@ export function DecisionApproachesScreen() { const da = m.create.customRule.decisionApproaches; const modalKebabMenu = m.create.customRule.modalKebabMenu; const mdUp = useCreateFlowMdUp(); + const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } = + useDiscardCustomizeConfirm(); const { state, updateState, replaceState, markCreateFlowInteraction } = useCreateFlow(); const pendingEphemeralDuplicateIdRef = useRef(null); @@ -216,16 +217,15 @@ export function DecisionApproachesScreen() { ], ); - const handleCreateModalClose = useCallback(() => { + const handleCreateModalClose = useCallback(async () => { if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, customizeSnapshotRef.current, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -256,15 +256,15 @@ export function DecisionApproachesScreen() { setDraftFieldBlocks(null); setCustomizeHeaderDraft(null); }, [ + confirmDiscard, customizeHeaderDraft, draftFieldBlocks, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, replaceState, ]); - const handleCancelCustomize = useCallback(() => { + const handleCancelCustomize = useCallback(async () => { if (!modalEditUnlocked) { return; } @@ -277,13 +277,12 @@ export function DecisionApproachesScreen() { return; } if ( - isMethodCardCustomizeSessionDirty( + !(await confirmDirtyCustomizeCancel( snap, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - ) && - !window.confirm(modalKebabMenu.discardUnsavedCustomizeChanges) + )) ) { return; } @@ -293,27 +292,26 @@ export function DecisionApproachesScreen() { customizeSnapshotRef.current = null; setCustomizeHeaderDraft(null); }, [ + confirmDirtyCustomizeCancel, customizeHeaderDraft, draftFieldBlocks, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, ]); - const handleRemoveSelectedFromModal = useCallback(() => { + const handleRemoveSelectedFromModal = useCallback(async () => { if (!pendingCardId || !selectedIds.includes(pendingCardId)) { return; } markCreateFlowInteraction(); if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, customizeSnapshotRef.current, pendingDraft, draftFieldBlocks, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -325,14 +323,14 @@ export function DecisionApproachesScreen() { pendingCardId, ), ); - handleCreateModalClose(); + await handleCreateModalClose(); }, [ + confirmDiscard, customizeHeaderDraft, draftFieldBlocks, handleCreateModalClose, markCreateFlowInteraction, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, pendingDraft, pendingCardId, selectedIds, @@ -867,6 +865,7 @@ export function DecisionApproachesScreen() { uploadCreateFlowFile(file, "customMethodAttachment") } /> + {confirmDialog} ); } diff --git a/app/(app)/create/screens/select/CoreValuesSelectScreen.tsx b/app/(app)/create/screens/select/CoreValuesSelectScreen.tsx index e141f77..61f04dd 100644 --- a/app/(app)/create/screens/select/CoreValuesSelectScreen.tsx +++ b/app/(app)/create/screens/select/CoreValuesSelectScreen.tsx @@ -8,6 +8,7 @@ import ContentLockup from "../../../../components/type/ContentLockup"; import { useMessages } from "../../../../contexts/MessagesContext"; import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft"; import { useCreateFlow } from "../../context/CreateFlowContext"; +import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm"; import type { CommunityStructureChipSnapshotRow, CoreValueDetailEntry, @@ -19,7 +20,6 @@ import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomize import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu"; import { captureMethodCardCustomizeSnapshot, - confirmDiscardMethodCardCustomizeSession, isMethodCardCustomizeSessionDirty, type MethodCardCustomizeSnapshot, type MethodCardHeaderDraft, @@ -101,6 +101,8 @@ export function CoreValuesSelectScreen() { [cv.values], ); + const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } = + useDiscardCustomizeConfirm(); const { markCreateFlowInteraction, updateState, replaceState, state } = useCreateFlow(); @@ -239,7 +241,7 @@ export function CoreValuesSelectScreen() { setModalEditUnlocked(true); }, [activeModalChipId, coreValueOptions, draft, markCreateFlowInteraction]); - const handleCancelCustomize = useCallback(() => { + const handleCancelCustomize = useCallback(async () => { if (!modalEditUnlocked) return; const snap = coreCustomizeSnapshotRef.current; if (!snap) { @@ -247,18 +249,22 @@ export function CoreValuesSelectScreen() { return; } if ( - isMethodCardCustomizeSessionDirty(snap, draft, null, customizeHeaderDraft) && - !window.confirm(modalKebabMenu.discardUnsavedCustomizeChanges) + !(await confirmDirtyCustomizeCancel( + snap, + draft, + null, + customizeHeaderDraft, + )) ) { return; } setDraft(structuredClone(snap.pendingDraft)); resetCustomizeSession(); }, [ + confirmDirtyCustomizeCancel, customizeHeaderDraft, draft, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, resetCustomizeSession, ]); @@ -271,17 +277,16 @@ export function CoreValuesSelectScreen() { ); }, [activeModalChipId, customizeHeaderDraft, coreValueOptions]); - const handleDuplicateCoreChip = useCallback(() => { + const handleDuplicateCoreChip = useCallback(async () => { if (!activeModalChipId || !modalSession) return; if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, coreCustomizeSnapshotRef.current, draft, null, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -317,11 +322,11 @@ export function CoreValuesSelectScreen() { ); }, [ activeModalChipId, + confirmDiscard, customizeHeaderDraft, draft, markCreateFlowInteraction, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, modalKebabMenu.duplicateTitleSuffix, modalSession, openModal, @@ -329,16 +334,15 @@ export function CoreValuesSelectScreen() { resetCustomizeSession, ]); - const handleRemoveFromKebab = useCallback(() => { + const handleRemoveFromKebab = useCallback(async () => { if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, coreCustomizeSnapshotRef.current, draft, null, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -382,30 +386,27 @@ export function CoreValuesSelectScreen() { finalizeModalDismiss(); }, [ activeModalChipId, + confirmDiscard, coreValueOptions, customizeHeaderDraft, draft, finalizeModalDismiss, markCreateFlowInteraction, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, modalSession, persistCoreValues, replaceState, - modalSession, - persistCoreValues, ]); - const handleModalDismiss = useCallback(() => { + const handleModalDismiss = useCallback(async () => { if ( - !confirmDiscardMethodCardCustomizeSession( + !(await confirmDiscard( modalEditUnlocked, coreCustomizeSnapshotRef.current, draft, null, customizeHeaderDraft, - modalKebabMenu.discardUnsavedCustomizeChanges, - ) + )) ) { return; } @@ -435,12 +436,12 @@ export function CoreValuesSelectScreen() { finalizeModalDismiss(); }, [ activeModalChipId, + confirmDiscard, coreValueOptions, customizeHeaderDraft, draft, finalizeModalDismiss, modalEditUnlocked, - modalKebabMenu.discardUnsavedCustomizeChanges, modalSession, persistCoreValues, replaceState, @@ -645,6 +646,7 @@ export function CoreValuesSelectScreen() { const detailModal = cv.detailModal; return ( + <> )} + {confirmDialog} + ); } diff --git a/app/components/cards/Rule/Rule.container.tsx b/app/components/cards/Rule/Rule.container.tsx index f367808..fd3503b 100644 --- a/app/components/cards/Rule/Rule.container.tsx +++ b/app/components/cards/Rule/Rule.container.tsx @@ -1,6 +1,7 @@ "use client"; import { memo } from "react"; +import { useTranslation } from "../../../contexts/MessagesContext"; import { RuleView } from "./Rule.view"; import type { RuleProps } from "./Rule.types"; @@ -49,6 +50,9 @@ const RuleContainer = memo( fluidWidth = false, }) => { const size = sizeProp ?? "L"; + const t = useTranslation("ruleCard"); + const cardAriaLabel = t("ariaLabel")?.replace("{title}", title) || title; + const recommendedLabel = t("recommendedLabel"); const handleClick = () => { if (hasBottomLinks) return; @@ -106,6 +110,8 @@ const RuleContainer = memo( recommended={recommended} templateGridFigmaShell={templateGridFigmaShell} fluidWidth={fluidWidth} + cardAriaLabel={cardAriaLabel} + recommendedLabel={recommendedLabel} /> ); }, diff --git a/app/components/cards/Rule/Rule.types.ts b/app/components/cards/Rule/Rule.types.ts index 27fecc2..efb1586 100644 --- a/app/components/cards/Rule/Rule.types.ts +++ b/app/components/cards/Rule/Rule.types.ts @@ -107,4 +107,7 @@ export interface RuleViewProps { recommended?: boolean; templateGridFigmaShell?: boolean; fluidWidth?: boolean; + /** Interactive card aria-label; supplied by the container from `ruleCard` messages. */ + cardAriaLabel: string; + recommendedLabel: string; } diff --git a/app/components/cards/Rule/Rule.view.tsx b/app/components/cards/Rule/Rule.view.tsx index 1982a06..a8ecfe9 100644 --- a/app/components/cards/Rule/Rule.view.tsx +++ b/app/components/cards/Rule/Rule.view.tsx @@ -1,7 +1,6 @@ "use client"; import Image from "next/image"; -import { useTranslation } from "../../../contexts/MessagesContext"; import MultiSelect from "../../controls/MultiSelect"; import InlineTextButton from "../../buttons/InlineTextButton"; import NavigationLink from "../../navigation/Link"; @@ -34,9 +33,10 @@ export function RuleView({ recommended = false, templateGridFigmaShell = false, fluidWidth = false, + cardAriaLabel, + recommendedLabel, }: RuleViewProps) { - const t = useTranslation("ruleCard"); - const ariaLabel = t("ariaLabel")?.replace("{title}", title) || title; + const ariaLabel = cardAriaLabel; const interactiveCard = !hasBottomLinks; // Size-based styling @@ -306,7 +306,7 @@ export function RuleView({ > {showRecommendedTag ? ( - {t("recommendedLabel")} + {recommendedLabel} ) : null} {onTitleClick ? ( diff --git a/app/components/localization/LanguageSwitcher/LanguageSwitcher.container.tsx b/app/components/localization/LanguageSwitcher/LanguageSwitcher.container.tsx deleted file mode 100644 index 5904192..0000000 --- a/app/components/localization/LanguageSwitcher/LanguageSwitcher.container.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -/** - * Figma: "localization/LanguageSwitcher" (see registry) - */ - -import { memo } from "react"; -import LanguageSwitcherView from "./LanguageSwitcher.view"; -import type { LanguageSwitcherProps } from "./LanguageSwitcher.types"; - -const LanguageSwitcherContainer = memo( - ({ className }) => { - // Future: Add language switching logic here - // For now, this is just a UI component - - return ; - }, -); - -LanguageSwitcherContainer.displayName = "LanguageSwitcher"; - -export default LanguageSwitcherContainer; diff --git a/app/components/localization/LanguageSwitcher/LanguageSwitcher.types.ts b/app/components/localization/LanguageSwitcher/LanguageSwitcher.types.ts deleted file mode 100644 index 5b6976b..0000000 --- a/app/components/localization/LanguageSwitcher/LanguageSwitcher.types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface LanguageSwitcherProps { - className?: string; -} - -export interface Language { - code: string; - name: string; - nativeName: string; -} diff --git a/app/components/localization/LanguageSwitcher/LanguageSwitcher.view.tsx b/app/components/localization/LanguageSwitcher/LanguageSwitcher.view.tsx deleted file mode 100644 index a5f138b..0000000 --- a/app/components/localization/LanguageSwitcher/LanguageSwitcher.view.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { memo } from "react"; -import { useTranslation } from "../../../contexts/MessagesContext"; -import type { LanguageSwitcherProps, Language } from "./LanguageSwitcher.types"; - -function LanguageSwitcherView({ className = "" }: LanguageSwitcherProps) { - const t = useTranslation("languageSwitcher"); - - const AVAILABLE_LANGUAGES: Language[] = [ - { - code: "en", - name: t("languages.english.name"), - nativeName: t("languages.english.nativeName"), - }, - ]; - - return ( -
- - -

- {t("comingSoonMessage")} -

-
- ); -} - -export default memo(LanguageSwitcherView); diff --git a/app/components/localization/LanguageSwitcher/index.tsx b/app/components/localization/LanguageSwitcher/index.tsx deleted file mode 100644 index 4ea8270..0000000 --- a/app/components/localization/LanguageSwitcher/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./LanguageSwitcher.container"; -export type { LanguageSwitcherProps, Language } from "./LanguageSwitcher.types"; diff --git a/app/components/modals/AskOrganizerInquiry/AskOrganizerInquiryModal.container.tsx b/app/components/modals/AskOrganizerInquiry/AskOrganizerInquiryModal.container.tsx index b381a20..64b89ec 100644 --- a/app/components/modals/AskOrganizerInquiry/AskOrganizerInquiryModal.container.tsx +++ b/app/components/modals/AskOrganizerInquiry/AskOrganizerInquiryModal.container.tsx @@ -5,7 +5,7 @@ * File: agv0VBLiBlcnSAaiAORgPR, node 22078-587823 */ -import { memo, useCallback, useEffect, useState, type FormEvent } from "react"; +import { memo, useCallback, useEffect, useMemo, useState, type FormEvent } from "react"; import { AskOrganizerInquiryModalView } from "./AskOrganizerInquiryModal.view"; import type { AskOrganizerInquiryModalProps } from "./AskOrganizerInquiryModal.types"; import { ORGANIZER_INQUIRY_HONEYPOT_FIELD } from "../../../../lib/organizerInquiryConstants"; @@ -14,6 +14,23 @@ import { useTranslation } from "../../../contexts/MessagesContext"; const AskOrganizerInquiryModalContainer = memo( ({ isOpen, onClose }) => { const t = useTranslation("modals.askOrganizerInquiry"); + const copy = useMemo( + () => ({ + title: t("title"), + description: t("description"), + emailLabel: t("emailLabel"), + emailPlaceholder: t("emailPlaceholder"), + questionLabel: t("questionLabel"), + questionPlaceholder: t("questionPlaceholder"), + submitButton: t("submitButton"), + closeAfterSuccess: t("closeAfterSuccess"), + successTitle: t("successTitle"), + successDescription: t("successDescription"), + ariaDialog: t("ariaDialog"), + honeypotLabel: t("honeypotLabel"), + }), + [t], + ); const [email, setEmail] = useState(""); const [message, setMessage] = useState(""); const [honeypot, setHoneypot] = useState(""); @@ -102,6 +119,7 @@ const AskOrganizerInquiryModalContainer = memo( void; } + +export interface AskOrganizerInquiryModalCopy { + title: string; + description: string; + emailLabel: string; + emailPlaceholder: string; + questionLabel: string; + questionPlaceholder: string; + submitButton: string; + closeAfterSuccess: string; + successTitle: string; + successDescription: string; + ariaDialog: string; + honeypotLabel: string; +} + +export interface AskOrganizerInquiryModalViewProps + extends AskOrganizerInquiryModalProps { + copy: AskOrganizerInquiryModalCopy; + email: string; + message: string; + honeypot: string; + submitting: boolean; + success: boolean; + formError: string | null; + emailError: boolean; + questionError: boolean; + onEmailChange: (_v: string) => void; + onMessageChange: (_v: string) => void; + onHoneypotChange: (_v: string) => void; + onSubmit: (_e: import("react").FormEvent) => void; +} diff --git a/app/components/modals/AskOrganizerInquiry/AskOrganizerInquiryModal.view.tsx b/app/components/modals/AskOrganizerInquiry/AskOrganizerInquiryModal.view.tsx index 906c3c3..f01f9ac 100644 --- a/app/components/modals/AskOrganizerInquiry/AskOrganizerInquiryModal.view.tsx +++ b/app/components/modals/AskOrganizerInquiry/AskOrganizerInquiryModal.view.tsx @@ -1,31 +1,14 @@ "use client"; -import type { FormEvent } from "react"; import Create from "../Create"; import TextInput from "../../controls/TextInput"; import TextArea from "../../controls/TextArea"; import Button from "../../buttons/Button"; -import { useTranslation } from "../../../contexts/MessagesContext"; import { ASK_ORGANIZER_INQUIRY_FORM_ID, ORGANIZER_INQUIRY_HONEYPOT_FIELD, } from "../../../../lib/organizerInquiryConstants"; -import type { AskOrganizerInquiryModalProps } from "./AskOrganizerInquiryModal.types"; - -export type AskOrganizerInquiryModalViewProps = AskOrganizerInquiryModalProps & { - email: string; - message: string; - honeypot: string; - submitting: boolean; - success: boolean; - formError: string | null; - emailError: boolean; - questionError: boolean; - onEmailChange: (_v: string) => void; - onMessageChange: (_v: string) => void; - onHoneypotChange: (_v: string) => void; - onSubmit: (_e: FormEvent) => void; -}; +import type { AskOrganizerInquiryModalViewProps } from "./AskOrganizerInquiryModal.types"; /** * Figma: Community Rule System — Modal / Ask an Organizer (22078-587823) @@ -33,6 +16,7 @@ export type AskOrganizerInquiryModalViewProps = AskOrganizerInquiryModalProps & export function AskOrganizerInquiryModalView({ isOpen, onClose, + copy, email, message, honeypot, @@ -46,8 +30,6 @@ export function AskOrganizerInquiryModalView({ onHoneypotChange, onSubmit, }: AskOrganizerInquiryModalViewProps) { - const t = useTranslation("modals.askOrganizerInquiry"); - const footer = success ? (
) : ( @@ -72,7 +54,7 @@ export function AskOrganizerInquiryModalView({ className="w-full !justify-center" disabled={submitting} > - {t("submitButton")} + {copy.submitButton} ); @@ -82,22 +64,22 @@ export function AskOrganizerInquiryModalView({ isOpen={isOpen} onClose={onClose} backdropVariant="blurredYellow" - title={t("title")} - description={t("description")} + title={copy.title} + description={copy.description} showBackButton={false} showNextButton={false} stepper={false} - ariaLabel={t("ariaDialog")} + ariaLabel={copy.ariaDialog} footerContent={footer} footerClassName="!h-auto min-h-[112px] shrink-0 flex flex-col justify-end pb-8 pt-3 px-4" > {success ? (

- {t("successTitle")} + {copy.successTitle}

- {t("successDescription")} + {copy.successDescription}

) : ( @@ -120,8 +102,8 @@ export function AskOrganizerInquiryModalView({ type="email" name="email" autoComplete="email" - label={t("emailLabel")} - placeholder={t("emailPlaceholder")} + label={copy.emailLabel} + placeholder={copy.emailPlaceholder} value={email} onChange={(e) => onEmailChange(e.target.value)} error={emailError} @@ -131,8 +113,8 @@ export function AskOrganizerInquiryModalView({