Cleanup pass 2
This commit is contained in:
@@ -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<Draft | null>(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 (
|
||||
<>
|
||||
<Create
|
||||
isOpen={isOpen}
|
||||
onClose={handleModalClose}
|
||||
@@ -1184,6 +1180,8 @@ export function FinalReviewChipEditModal({
|
||||
))}
|
||||
</div>
|
||||
</Create>
|
||||
{confirmDialog}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<boolean>;
|
||||
}): (_options?: { saveDraft?: boolean }) => Promise<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 <TDraft,>(
|
||||
modalEditUnlocked: boolean,
|
||||
snapshot: MethodCardCustomizeSnapshot<TDraft> | null,
|
||||
pendingDraft: TDraft | null,
|
||||
draftFieldBlocks: CustomMethodCardFieldBlock[] | null,
|
||||
headerDraft: MethodCardHeaderDraft | null,
|
||||
) =>
|
||||
confirmDiscardMethodCardCustomizeSession(
|
||||
modalEditUnlocked,
|
||||
snapshot,
|
||||
pendingDraft,
|
||||
draftFieldBlocks,
|
||||
headerDraft,
|
||||
runConfirm,
|
||||
),
|
||||
[runConfirm],
|
||||
);
|
||||
|
||||
const confirmDirtyCustomizeCancel = useCallback(
|
||||
async <TDraft,>(
|
||||
snapshot: MethodCardCustomizeSnapshot<TDraft>,
|
||||
pendingDraft: TDraft | null,
|
||||
draftFieldBlocks: CustomMethodCardFieldBlock[] | null,
|
||||
headerDraft: MethodCardHeaderDraft | null,
|
||||
) => {
|
||||
if (
|
||||
!isMethodCardCustomizeSessionDirty(
|
||||
snapshot,
|
||||
pendingDraft,
|
||||
draftFieldBlocks,
|
||||
headerDraft,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return runConfirm();
|
||||
},
|
||||
[runConfirm],
|
||||
);
|
||||
|
||||
return { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog };
|
||||
}
|
||||
@@ -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<string | null>(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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string | null>(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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string | null>(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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string | null>(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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<CreateFlowTwoColumnSelectShell
|
||||
lgVerticalAlign="start"
|
||||
header={
|
||||
@@ -724,5 +726,7 @@ export function CoreValuesSelectScreen() {
|
||||
</Create>
|
||||
)}
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
{confirmDialog}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<RuleProps>(
|
||||
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<RuleProps>(
|
||||
recommended={recommended}
|
||||
templateGridFigmaShell={templateGridFigmaShell}
|
||||
fluidWidth={fluidWidth}
|
||||
cardAriaLabel={cardAriaLabel}
|
||||
recommendedLabel={recommendedLabel}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 ? (
|
||||
<Tag variant="templateRecommended">
|
||||
{t("recommendedLabel")}
|
||||
{recommendedLabel}
|
||||
</Tag>
|
||||
) : null}
|
||||
{onTitleClick ? (
|
||||
|
||||
@@ -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<LanguageSwitcherProps>(
|
||||
({ className }) => {
|
||||
// Future: Add language switching logic here
|
||||
// For now, this is just a UI component
|
||||
|
||||
return <LanguageSwitcherView className={className} />;
|
||||
},
|
||||
);
|
||||
|
||||
LanguageSwitcherContainer.displayName = "LanguageSwitcher";
|
||||
|
||||
export default LanguageSwitcherContainer;
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface LanguageSwitcherProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface Language {
|
||||
code: string;
|
||||
name: string;
|
||||
nativeName: string;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={className}>
|
||||
<label htmlFor="language-select" className="sr-only">
|
||||
{t("label")}
|
||||
</label>
|
||||
<select
|
||||
id="language-select"
|
||||
className="bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] font-inter text-sm leading-5 font-normal border border-[var(--color-surface-default-secondary)] rounded-[var(--radius-measures-radius-small)] px-[var(--spacing-scale-012)] py-[var(--spacing-scale-008)] focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 cursor-pointer"
|
||||
aria-label={t("ariaLabel")}
|
||||
disabled
|
||||
>
|
||||
{AVAILABLE_LANGUAGES.map((language) => (
|
||||
<option key={language.code} value={language.code}>
|
||||
{language.nativeName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-[var(--color-content-default-secondary)] font-inter text-xs leading-4 font-normal mt-[var(--spacing-scale-008)]">
|
||||
{t("comingSoonMessage")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LanguageSwitcherView);
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from "./LanguageSwitcher.container";
|
||||
export type { LanguageSwitcherProps, Language } from "./LanguageSwitcher.types";
|
||||
@@ -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<AskOrganizerInquiryModalProps>(
|
||||
({ 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<AskOrganizerInquiryModalProps>(
|
||||
<AskOrganizerInquiryModalView
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
copy={copy}
|
||||
email={email}
|
||||
message={message}
|
||||
honeypot={honeypot}
|
||||
|
||||
@@ -2,3 +2,35 @@ export interface AskOrganizerInquiryModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => 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<HTMLFormElement>) => void;
|
||||
}
|
||||
|
||||
@@ -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<HTMLFormElement>) => 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 ? (
|
||||
<div className="w-full px-1">
|
||||
<Button
|
||||
@@ -58,7 +40,7 @@ export function AskOrganizerInquiryModalView({
|
||||
className="w-full !justify-center"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("closeAfterSuccess")}
|
||||
{copy.closeAfterSuccess}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -72,7 +54,7 @@ export function AskOrganizerInquiryModalView({
|
||||
className="w-full !justify-center"
|
||||
disabled={submitting}
|
||||
>
|
||||
{t("submitButton")}
|
||||
{copy.submitButton}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -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 ? (
|
||||
<div className="flex flex-col gap-3 py-2">
|
||||
<p className="font-inter text-[18px] font-semibold leading-[24px] text-[var(--color-content-default-primary)]">
|
||||
{t("successTitle")}
|
||||
{copy.successTitle}
|
||||
</p>
|
||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)]">
|
||||
{t("successDescription")}
|
||||
{copy.successDescription}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -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({
|
||||
|
||||
<TextArea
|
||||
name="message"
|
||||
label={t("questionLabel")}
|
||||
placeholder={t("questionPlaceholder")}
|
||||
label={copy.questionLabel}
|
||||
placeholder={copy.questionPlaceholder}
|
||||
value={message}
|
||||
onChange={(e) => onMessageChange(e.target.value)}
|
||||
error={questionError}
|
||||
@@ -146,7 +128,7 @@ export function AskOrganizerInquiryModalView({
|
||||
className="pointer-events-none absolute left-0 top-0 h-px w-px overflow-hidden opacity-0"
|
||||
>
|
||||
<label htmlFor={`${ASK_ORGANIZER_INQUIRY_FORM_ID}-${ORGANIZER_INQUIRY_HONEYPOT_FIELD}`}>
|
||||
{t("honeypotLabel")}
|
||||
{copy.honeypotLabel}
|
||||
</label>
|
||||
<input
|
||||
id={`${ASK_ORGANIZER_INQUIRY_FORM_ID}-${ORGANIZER_INQUIRY_HONEYPOT_FIELD}`}
|
||||
|
||||
@@ -262,14 +262,14 @@ export default function LoginForm({
|
||||
<p className="text-center font-inter text-[14px] leading-[20px] text-[var(--color-content-default-tertiary)]">
|
||||
{t("legalPrefix")}
|
||||
<Link
|
||||
href="#"
|
||||
href={tFooter("legal.termsOfServiceHref")}
|
||||
className="text-[var(--color-content-default-tertiary)] underline decoration-solid underline-offset-2"
|
||||
>
|
||||
{tFooter("legal.termsOfService")}
|
||||
</Link>
|
||||
{t("legalAnd")}
|
||||
<Link
|
||||
href="#"
|
||||
href={tFooter("legal.privacyPolicyHref")}
|
||||
className="text-[var(--color-content-default-tertiary)] underline decoration-solid underline-offset-2"
|
||||
>
|
||||
{tFooter("legal.privacyPolicy")}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { ModalFooterView } from "./ModalFooter.view";
|
||||
import type { ModalFooterProps } from "./ModalFooter.types";
|
||||
|
||||
@@ -10,7 +11,17 @@ import type { ModalFooterProps } from "./ModalFooter.types";
|
||||
* primary/secondary actions.
|
||||
*/
|
||||
const ModalFooterContainer = memo<ModalFooterProps>((props) => {
|
||||
return <ModalFooterView {...props} />;
|
||||
const t = useTranslation("common");
|
||||
const resolvedBackText = props.backButtonText ?? t("buttons.back");
|
||||
const resolvedNextText = props.nextButtonText ?? t("buttons.next");
|
||||
|
||||
return (
|
||||
<ModalFooterView
|
||||
{...props}
|
||||
backButtonText={resolvedBackText}
|
||||
nextButtonText={resolvedNextText}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ModalFooterContainer.displayName = "ModalFooter";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import Button from "../../buttons/Button";
|
||||
import Stepper from "../../progress/Stepper";
|
||||
import type { ModalFooterProps } from "./ModalFooter.types";
|
||||
@@ -19,14 +18,6 @@ export function ModalFooterView({
|
||||
footerContent,
|
||||
className = "",
|
||||
}: ModalFooterProps) {
|
||||
const t = useTranslation("common");
|
||||
|
||||
// Use localized defaults if text not provided
|
||||
const defaultBackText = backButtonText || t("buttons.back");
|
||||
const defaultNextText = nextButtonText || t("buttons.next");
|
||||
|
||||
// Determine if stepper should be shown
|
||||
// Defaults to true if currentStep and totalSteps are provided, unless explicitly set to false
|
||||
const shouldShowStepper =
|
||||
stepperProp !== undefined
|
||||
? stepperProp
|
||||
@@ -36,7 +27,6 @@ export function ModalFooterView({
|
||||
<div
|
||||
className={`h-[64px] bg-[var(--color-surface-default-primary)] rounded-bl-[var(--radius-300,12px)] rounded-br-[var(--radius-300,12px)] shrink-0 relative ${className}`}
|
||||
>
|
||||
{/* Back Button - Absolutely positioned bottom left */}
|
||||
{showBackButton && (
|
||||
<div className="absolute left-[16px] top-[12px]">
|
||||
<Button
|
||||
@@ -45,19 +35,17 @@ export function ModalFooterView({
|
||||
size="medium"
|
||||
onClick={onBack}
|
||||
>
|
||||
{defaultBackText}
|
||||
{backButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stepper (Centered) */}
|
||||
{shouldShowStepper && currentStep && totalSteps && (
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Stepper active={currentStep} totalSteps={totalSteps} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Next Button - Absolutely positioned bottom right */}
|
||||
{showNextButton && (
|
||||
<div className="absolute right-[16px] top-[12px]">
|
||||
<Button
|
||||
@@ -67,12 +55,11 @@ export function ModalFooterView({
|
||||
onClick={onNext}
|
||||
disabled={nextButtonDisabled}
|
||||
>
|
||||
{defaultNextText}
|
||||
{nextButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Footer Content */}
|
||||
{footerContent}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -237,6 +237,16 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
|
||||
exportPopoverMarkdownLabel={tPopover("downloadMarkdown")}
|
||||
moreOptionsAriaLabel={t("moreOptionsAriaLabel")}
|
||||
actionsMenuAriaLabel={t("actionsMenuAriaLabel")}
|
||||
shareLabel={t("share")}
|
||||
exportLabel={t("export")}
|
||||
editLabel={t("edit")}
|
||||
manageStakeholdersLabel={t("manageStakeholders")}
|
||||
shareAriaLabel={t("shareAriaLabel")}
|
||||
exportAriaLabel={t("exportAriaLabel")}
|
||||
editAriaLabel={t("editAriaLabel")}
|
||||
manageStakeholdersAriaLabel={t("manageStakeholdersAriaLabel")}
|
||||
bannerAriaLabel={t("bannerAriaLabel")}
|
||||
navAriaLabel={t("navAriaLabel")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -110,4 +110,14 @@ export type CreateFlowTopNavViewProps = CreateFlowTopNavProps & {
|
||||
exportPopoverMarkdownLabel: string;
|
||||
moreOptionsAriaLabel: string;
|
||||
actionsMenuAriaLabel: string;
|
||||
shareLabel: string;
|
||||
exportLabel: string;
|
||||
editLabel: string;
|
||||
manageStakeholdersLabel: string;
|
||||
shareAriaLabel: string;
|
||||
exportAriaLabel: string;
|
||||
editAriaLabel: string;
|
||||
manageStakeholdersAriaLabel: string;
|
||||
bannerAriaLabel: string;
|
||||
navAriaLabel: string;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import Logo from "../../asset/Logo";
|
||||
import Button from "../../buttons/Button";
|
||||
import ListItem from "../../layout/ListItem";
|
||||
import Popover from "../../modals/Popover";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import type { CreateFlowTopNavViewProps } from "./CreateFlowTopNav.types";
|
||||
|
||||
const outlineButtonClass =
|
||||
@@ -65,9 +64,17 @@ export function CreateFlowTopNavView({
|
||||
exportPopoverMarkdownLabel,
|
||||
moreOptionsAriaLabel,
|
||||
actionsMenuAriaLabel,
|
||||
shareLabel,
|
||||
exportLabel,
|
||||
editLabel,
|
||||
manageStakeholdersLabel,
|
||||
shareAriaLabel,
|
||||
exportAriaLabel,
|
||||
editAriaLabel,
|
||||
manageStakeholdersAriaLabel,
|
||||
bannerAriaLabel,
|
||||
navAriaLabel,
|
||||
}: CreateFlowTopNavViewProps) {
|
||||
const t = useTranslation("create.topNav");
|
||||
|
||||
const hasSecondaryActions =
|
||||
hasShare ||
|
||||
hasExport ||
|
||||
@@ -83,10 +90,10 @@ export function CreateFlowTopNavView({
|
||||
palette={buttonPalette}
|
||||
size="xsmall"
|
||||
onClick={onShare}
|
||||
ariaLabel={t("shareAriaLabel")}
|
||||
ariaLabel={shareAriaLabel}
|
||||
className={outlineButtonClass}
|
||||
>
|
||||
{t("share")}
|
||||
{shareLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -97,14 +104,14 @@ export function CreateFlowTopNavView({
|
||||
palette={buttonPalette}
|
||||
size="xsmall"
|
||||
type="button"
|
||||
ariaLabel={t("exportAriaLabel")}
|
||||
ariaLabel={exportAriaLabel}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={exportMenuOpen}
|
||||
aria-controls={exportMenuId}
|
||||
onClick={() => setExportMenuOpen((o) => !o)}
|
||||
className={`justify-center gap-[var(--spacing-scale-002,2px)] !pl-[var(--spacing-scale-012,12px)] !pr-[var(--spacing-scale-006,6px)] md:!pr-[var(--spacing-scale-006,6px)] ${outlineButtonClass}`}
|
||||
>
|
||||
<span>{t("export")}</span>
|
||||
<span>{exportLabel}</span>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
@@ -166,11 +173,11 @@ export function CreateFlowTopNavView({
|
||||
size="xsmall"
|
||||
onClick={onDuplicate}
|
||||
ariaLabel={
|
||||
duplicateAriaLabel ?? duplicateLabel ?? t("editAriaLabel")
|
||||
duplicateAriaLabel ?? duplicateLabel ?? editAriaLabel
|
||||
}
|
||||
className={outlineButtonClass}
|
||||
>
|
||||
{duplicateLabel ?? t("edit")}
|
||||
{duplicateLabel ?? editLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -180,10 +187,10 @@ export function CreateFlowTopNavView({
|
||||
palette={buttonPalette}
|
||||
size="xsmall"
|
||||
onClick={onEdit}
|
||||
ariaLabel={t("editAriaLabel")}
|
||||
ariaLabel={editAriaLabel}
|
||||
className={outlineButtonClass}
|
||||
>
|
||||
{t("edit")}
|
||||
{editLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -194,10 +201,10 @@ export function CreateFlowTopNavView({
|
||||
size="xsmall"
|
||||
type="button"
|
||||
onClick={onManageStakeholders}
|
||||
ariaLabel={t("manageStakeholdersAriaLabel")}
|
||||
ariaLabel={manageStakeholdersAriaLabel}
|
||||
className={outlineButtonClass}
|
||||
>
|
||||
{t("manageStakeholders")}
|
||||
{manageStakeholdersLabel}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
@@ -219,12 +226,12 @@ export function CreateFlowTopNavView({
|
||||
<header
|
||||
className={`bg-black w-full ${className}`}
|
||||
role="banner"
|
||||
aria-label={t("bannerAriaLabel")}
|
||||
aria-label={bannerAriaLabel}
|
||||
>
|
||||
<nav
|
||||
className="flex items-center justify-between mx-auto max-w-[639px] md:max-w-[1920px] px-[var(--spacing-measures-spacing-500,20px)] md:px-[48px] py-[var(--spacing-measures-spacing-300,12px)] md:py-[var(--spacing-measures-spacing-016,16px)]"
|
||||
role="navigation"
|
||||
aria-label={t("navAriaLabel")}
|
||||
aria-label={navAriaLabel}
|
||||
>
|
||||
<Logo size="createFlow" wordmark palette={buttonPalette} />
|
||||
|
||||
|
||||
@@ -192,13 +192,13 @@ const Footer = memo(() => {
|
||||
lg:gap-10
|
||||
lg:text-sm lg:leading-5"
|
||||
>
|
||||
<Link href="#" className={legalLinkClass}>
|
||||
<Link href={t("legal.privacyPolicyHref")} className={legalLinkClass}>
|
||||
{t("legal.privacyPolicy")}
|
||||
</Link>
|
||||
<Link href="#" className={legalLinkClass}>
|
||||
<Link href={t("legal.termsOfServiceHref")} className={legalLinkClass}>
|
||||
{t("legal.termsOfService")}
|
||||
</Link>
|
||||
<Link href="#" className={legalLinkClass}>
|
||||
<Link href={t("legal.cookiesSettingsHref")} className={legalLinkClass}>
|
||||
{t("legal.cookiesSettings")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Sections / AskOrganizer" (see registry).
|
||||
*/
|
||||
import { memo, useCallback, useState } from "react";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { useAnalytics } from "../../../hooks";
|
||||
@@ -43,7 +46,6 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
|
||||
subtitle,
|
||||
description,
|
||||
buttonText,
|
||||
buttonHref,
|
||||
className = "",
|
||||
variant: variantProp = "centered",
|
||||
onContactClick,
|
||||
@@ -51,7 +53,7 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
|
||||
const variant = variantProp;
|
||||
const t = useTranslation();
|
||||
const defaultButtonText = buttonText ?? t("askOrganizer.buttonText");
|
||||
const analyticsHref = buttonHref ?? "modal";
|
||||
const ctaAriaLabel = t("askOrganizer.ariaLabel");
|
||||
const { trackEvent, trackCustomEvent } = useAnalytics();
|
||||
const [inquiryOpen, setInquiryOpen] = useState(false);
|
||||
|
||||
@@ -74,11 +76,9 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
|
||||
|
||||
const labelledBy = title ? "ask-organizer-headline" : undefined;
|
||||
|
||||
const handleContactClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
|
||||
) => {
|
||||
if (buttonHref) {
|
||||
// Legacy link CTA: do not intercept navigation.
|
||||
const handleContactClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
trackEvent({
|
||||
event: "contact_button_click",
|
||||
category: "engagement",
|
||||
@@ -86,46 +86,30 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
|
||||
component: "AskOrganizer",
|
||||
variant: resolvedVariant,
|
||||
});
|
||||
|
||||
trackCustomEvent(
|
||||
"contact_button_click",
|
||||
{
|
||||
component: "AskOrganizer",
|
||||
variant: resolvedVariant,
|
||||
buttonText: defaultButtonText,
|
||||
buttonHref: analyticsHref,
|
||||
buttonHref: "modal",
|
||||
},
|
||||
onContactClick as
|
||||
| ((_data: Record<string, unknown>) => void)
|
||||
| undefined,
|
||||
);
|
||||
return event;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
trackEvent({
|
||||
event: "contact_button_click",
|
||||
category: "engagement",
|
||||
label: "ask_organizer",
|
||||
component: "AskOrganizer",
|
||||
variant: resolvedVariant,
|
||||
});
|
||||
|
||||
trackCustomEvent(
|
||||
"contact_button_click",
|
||||
{
|
||||
component: "AskOrganizer",
|
||||
variant: resolvedVariant,
|
||||
buttonText: defaultButtonText,
|
||||
buttonHref: analyticsHref,
|
||||
},
|
||||
onContactClick as
|
||||
| ((_data: Record<string, unknown>) => void)
|
||||
| undefined,
|
||||
);
|
||||
|
||||
setInquiryOpen(true);
|
||||
return event;
|
||||
};
|
||||
setInquiryOpen(true);
|
||||
},
|
||||
[
|
||||
defaultButtonText,
|
||||
onContactClick,
|
||||
resolvedVariant,
|
||||
trackCustomEvent,
|
||||
trackEvent,
|
||||
],
|
||||
);
|
||||
|
||||
const closeInquiry = useCallback(() => {
|
||||
setInquiryOpen(false);
|
||||
@@ -138,7 +122,7 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
|
||||
subtitle={subtitle}
|
||||
description={description}
|
||||
buttonText={defaultButtonText}
|
||||
buttonHref={buttonHref}
|
||||
ctaAriaLabel={ctaAriaLabel}
|
||||
className={className}
|
||||
sectionPadding={sectionPadding}
|
||||
contentGap={`${contentGap} ${styles.container}`}
|
||||
|
||||
@@ -12,10 +12,6 @@ export interface AskOrganizerProps {
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
buttonText?: string;
|
||||
/**
|
||||
* @deprecated Modal-only flow (CR-107). Omit; kept optional for Storybook overrides.
|
||||
*/
|
||||
buttonHref?: string;
|
||||
className?: string;
|
||||
/**
|
||||
* Ask organizer variant.
|
||||
@@ -26,7 +22,6 @@ export interface AskOrganizerProps {
|
||||
component: string;
|
||||
variant: string;
|
||||
buttonText: string;
|
||||
buttonHref?: string;
|
||||
timestamp: string;
|
||||
}) => void;
|
||||
}
|
||||
@@ -36,7 +31,7 @@ export interface AskOrganizerViewProps {
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
buttonText: string;
|
||||
buttonHref?: string;
|
||||
ctaAriaLabel: string;
|
||||
className: string;
|
||||
sectionPadding: string;
|
||||
contentGap: string;
|
||||
@@ -44,6 +39,6 @@ export interface AskOrganizerViewProps {
|
||||
variant: AskOrganizerVariant;
|
||||
labelledBy?: string;
|
||||
onContactClick: (
|
||||
_event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
|
||||
_event: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import ContentLockup from "../../type/ContentLockup";
|
||||
import Button from "../../buttons/Button";
|
||||
import type { AskOrganizerViewProps } from "./AskOrganizer.types";
|
||||
@@ -10,7 +9,7 @@ function AskOrganizerView({
|
||||
subtitle,
|
||||
description,
|
||||
buttonText,
|
||||
buttonHref,
|
||||
ctaAriaLabel,
|
||||
className,
|
||||
sectionPadding,
|
||||
contentGap,
|
||||
@@ -19,8 +18,6 @@ function AskOrganizerView({
|
||||
labelledBy,
|
||||
onContactClick,
|
||||
}: AskOrganizerViewProps) {
|
||||
const t = useTranslation();
|
||||
const ariaLabel = t("askOrganizer.ariaLabel");
|
||||
const isUseCaseDetail = variant === "use-case-detail";
|
||||
const lockupVariant =
|
||||
variant === "inverse" || isUseCaseDetail ? "ask-inverse" : "ask";
|
||||
@@ -33,14 +30,13 @@ function AskOrganizerView({
|
||||
<section
|
||||
className={`${sectionPadding} ${className}`}
|
||||
aria-labelledby={labelledBy}
|
||||
aria-label={labelledBy ? undefined : ariaLabel}
|
||||
aria-label={labelledBy ? undefined : ctaAriaLabel}
|
||||
tabIndex={-1}
|
||||
data-figma-node={isUseCaseDetail ? "22015-42624" : "18116-15960"}
|
||||
>
|
||||
<div
|
||||
className={`mx-auto flex w-full min-w-0 max-w-[1280px] flex-col md:min-w-[358px] ${contentGap} ${isUseCaseDetail ? "items-center" : ""}`}
|
||||
>
|
||||
{/* Content Lockup */}
|
||||
<ContentLockup
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
@@ -50,18 +46,16 @@ function AskOrganizerView({
|
||||
titleId={labelledBy}
|
||||
/>
|
||||
|
||||
{/* Button */}
|
||||
<div
|
||||
className={`${buttonContainerClass} flex-wrap gap-y-[var(--spacing-scale-016)]`}
|
||||
>
|
||||
<Button
|
||||
{...(buttonHref ? { href: buttonHref } : {})}
|
||||
size="small"
|
||||
buttonType="filled"
|
||||
palette={buttonPalette}
|
||||
className="!px-[var(--spacing-scale-010)] md:!px-[var(--spacing-scale-016)] md:!py-[var(--spacing-scale-012)] md:!text-[16px] md:!leading-[20px]"
|
||||
onClick={onContactClick}
|
||||
ariaLabel={ariaLabel}
|
||||
ariaLabel={ctaAriaLabel}
|
||||
data-testid="ask-organizer-cta"
|
||||
>
|
||||
{buttonText}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import {
|
||||
ASSETS,
|
||||
getAssetPath,
|
||||
@@ -23,6 +24,14 @@ const ContentBannerContainer = memo<ContentBannerProps>(
|
||||
contentTone,
|
||||
}) => {
|
||||
const variant = variantProp;
|
||||
const tUseCase = useTranslation("pages.useCasesCompletedRule");
|
||||
const ruleCardLinkAriaLabel =
|
||||
variant === "useCase" && rulePreview?.href
|
||||
? tUseCase("ruleCardLinkAriaLabel").replace(
|
||||
"{title}",
|
||||
rulePreview.title,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const resolveHorizontalImage = (blogPost: BlogPost): string => {
|
||||
if (blogPost.frontmatter?.thumbnail?.horizontal) {
|
||||
@@ -71,6 +80,7 @@ const ContentBannerContainer = memo<ContentBannerProps>(
|
||||
backgroundImageSection={backgroundImageSection}
|
||||
rulePreview={rulePreview}
|
||||
contentTone={contentTone}
|
||||
ruleCardLinkAriaLabel={ruleCardLinkAriaLabel}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -41,4 +41,6 @@ export interface ContentBannerViewProps {
|
||||
backgroundImageSection?: string;
|
||||
rulePreview?: ContentBannerRulePreview;
|
||||
contentTone?: ContentContainerToneValue;
|
||||
/** `useCase` only: aria-label for linked rule preview. */
|
||||
ruleCardLinkAriaLabel?: string;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import ContentContainer from "../../content/ContentContainer";
|
||||
import Rule from "../../cards/Rule";
|
||||
import {
|
||||
@@ -155,6 +154,7 @@ function ContentBannerUseCaseView({
|
||||
contentTone = "inverse",
|
||||
leadingImageSrc,
|
||||
leadingImageAlt,
|
||||
ruleCardLinkAriaLabel,
|
||||
}: Pick<
|
||||
ContentBannerViewProps,
|
||||
| "post"
|
||||
@@ -162,8 +162,8 @@ function ContentBannerUseCaseView({
|
||||
| "contentTone"
|
||||
| "leadingImageSrc"
|
||||
| "leadingImageAlt"
|
||||
| "ruleCardLinkAriaLabel"
|
||||
>) {
|
||||
const t = useTranslation("pages.useCasesCompletedRule");
|
||||
if (!rulePreview) {
|
||||
return null;
|
||||
}
|
||||
@@ -198,10 +198,7 @@ function ContentBannerUseCaseView({
|
||||
<Link
|
||||
href={rulePreview.href}
|
||||
className="block w-full rounded-[24px] outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-default-brand-primary)] focus-visible:ring-offset-2"
|
||||
aria-label={t("ruleCardLinkAriaLabel").replace(
|
||||
"{title}",
|
||||
rulePreview.title,
|
||||
)}
|
||||
aria-label={ruleCardLinkAriaLabel ?? rulePreview.title}
|
||||
>
|
||||
<Rule
|
||||
title={rulePreview.title}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { logger } from "../../../../lib/logger";
|
||||
import { prepareFreshCreateFlowEntry } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
|
||||
import {
|
||||
@@ -34,6 +35,8 @@ declare global {
|
||||
const RuleStackContainer = memo<RuleStackProps>(
|
||||
({ className = "", initialGridEntries, translationNamespace, twoColumnsFromMd }) => {
|
||||
const router = useRouter();
|
||||
const namespace = translationNamespace ?? "pages.home.ruleStack";
|
||||
const t = useTranslation(namespace);
|
||||
const [gridEntries, setGridEntries] = useState<TemplateGridCardEntry[] | null>(
|
||||
() => initialGridEntries ?? null,
|
||||
);
|
||||
@@ -107,7 +110,9 @@ const RuleStackContainer = memo<RuleStackProps>(
|
||||
className={className}
|
||||
onTemplateClick={handleTemplateClick}
|
||||
gridEntries={gridEntries}
|
||||
translationNamespace={translationNamespace ?? "pages.home.ruleStack"}
|
||||
sectionTitle={t("title")}
|
||||
sectionSubtitle={t("subtitle")}
|
||||
seeAllTemplatesLabel={t("button.seeAllTemplates")}
|
||||
twoColumnsFromMd={twoColumnsFromMd}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface RuleStackViewProps {
|
||||
onTemplateClick: (_slug: string) => void;
|
||||
/** `null` while loading curated templates from the API. */
|
||||
gridEntries: TemplateGridCardEntry[] | null;
|
||||
translationNamespace: string;
|
||||
sectionTitle: string;
|
||||
sectionSubtitle: string;
|
||||
seeAllTemplatesLabel: string;
|
||||
twoColumnsFromMd?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import SectionHeader from "../../type/SectionHeader";
|
||||
import Button from "../../buttons/Button";
|
||||
import { GovernanceTemplateGrid } from "../GovernanceTemplateGrid";
|
||||
@@ -12,12 +11,11 @@ export function RuleStackView({
|
||||
className,
|
||||
onTemplateClick,
|
||||
gridEntries,
|
||||
translationNamespace,
|
||||
sectionTitle,
|
||||
sectionSubtitle,
|
||||
seeAllTemplatesLabel,
|
||||
twoColumnsFromMd = false,
|
||||
}: RuleStackViewProps) {
|
||||
const t = useTranslation(translationNamespace);
|
||||
const buttonText = t("button.seeAllTemplates");
|
||||
|
||||
return (
|
||||
<section
|
||||
data-figma-node="22085-860413"
|
||||
@@ -35,8 +33,8 @@ export function RuleStackView({
|
||||
`}
|
||||
>
|
||||
<SectionHeader
|
||||
title={t("title")}
|
||||
subtitle={t("subtitle")}
|
||||
title={sectionTitle}
|
||||
subtitle={sectionSubtitle}
|
||||
variant="multi-line"
|
||||
ruleStackDesktopTypeScale
|
||||
twoColumnsFromMd={twoColumnsFromMd}
|
||||
@@ -69,7 +67,7 @@ export function RuleStackView({
|
||||
size="large"
|
||||
href="/templates"
|
||||
>
|
||||
{buttonText}
|
||||
{seeAllTemplatesLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+3
-2
@@ -11,12 +11,13 @@ export { useAnalytics } from "./useAnalytics";
|
||||
export { useComponentId } from "./useComponentId";
|
||||
export { useFormField } from "./useFormField";
|
||||
export { useSchemaData } from "./useSchemaData";
|
||||
export {
|
||||
useMediaQuery,
|
||||
export { useMediaQuery,
|
||||
useIsMobile,
|
||||
useIsDesktop,
|
||||
BREAKPOINTS,
|
||||
} from "./useMediaQuery";
|
||||
export { useAsyncConfirm } from "./useAsyncConfirm";
|
||||
export type { AsyncConfirmOptions } from "./useAsyncConfirm";
|
||||
export type {
|
||||
SchemaOrganization,
|
||||
SchemaWebSite,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import Button from "../components/buttons/Button";
|
||||
import Create from "../components/modals/Create";
|
||||
import type { CreateModalBackdropVariant } from "../components/modals/Create/CreateModalFrame.view";
|
||||
|
||||
export type AsyncConfirmOptions = {
|
||||
title: string;
|
||||
description: string;
|
||||
proceedText: string;
|
||||
cancelText: string;
|
||||
ariaLabel?: string;
|
||||
backdropVariant?: CreateModalBackdropVariant;
|
||||
};
|
||||
|
||||
/**
|
||||
* Promise-based confirm dialog backed by the Create modal shell.
|
||||
*
|
||||
* @returns `requestConfirm` resolves true when the user proceeds, false on cancel.
|
||||
* Render `confirmDialog` once near the root of the consuming component tree.
|
||||
*
|
||||
* @example
|
||||
* const { requestConfirm, confirmDialog } = useAsyncConfirm();
|
||||
* if (!(await requestConfirm({ title: "Leave?", description: "...", proceedText: "Leave", cancelText: "Stay" }))) return;
|
||||
* return <>{confirmDialog}</>;
|
||||
*/
|
||||
export function useAsyncConfirm() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [options, setOptions] = useState<AsyncConfirmOptions | null>(null);
|
||||
const resolverRef = useRef<((proceed: boolean) => void) | null>(null);
|
||||
|
||||
const requestConfirm = useCallback((opts: AsyncConfirmOptions) => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolverRef.current = resolve;
|
||||
setOptions(opts);
|
||||
setOpen(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const close = useCallback((proceed: boolean) => {
|
||||
setOpen(false);
|
||||
const resolve = resolverRef.current;
|
||||
resolverRef.current = null;
|
||||
setOptions(null);
|
||||
resolve?.(proceed);
|
||||
}, []);
|
||||
|
||||
const confirmDialog =
|
||||
open && options ? (
|
||||
<Create
|
||||
isOpen={open}
|
||||
onClose={() => close(false)}
|
||||
title={options.title}
|
||||
description={options.description}
|
||||
showBackButton={false}
|
||||
showNextButton
|
||||
nextButtonText={options.proceedText}
|
||||
onNext={() => close(true)}
|
||||
footerContent={
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
onClick={() => close(false)}
|
||||
>
|
||||
{options.cancelText}
|
||||
</Button>
|
||||
}
|
||||
backdropVariant={options.backdropVariant ?? "blurredYellow"}
|
||||
ariaLabel={options.ariaLabel ?? options.title}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return { requestConfirm, confirmDialog };
|
||||
}
|
||||
+10
-11
@@ -38,17 +38,18 @@ const spaceGrotesk = Space_Grotesk({
|
||||
fallback: ["system-ui", "arial"],
|
||||
});
|
||||
|
||||
/** Viewport and favicon use the Metadata / Viewport APIs; avoid a manual `<head>` with a second viewport `meta` (duplicates Next’s head injection). */
|
||||
const homeMeta = messages.metadata.home;
|
||||
|
||||
/** Viewport and favicon use the Metadata / Viewport APIs; avoid a manual `<head>` with a second viewport `meta` (duplicates Next's head injection). */
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "CommunityRule - Build operating manuals for successful communities",
|
||||
description:
|
||||
"Help your community make important decisions in a way that reflects its unique values.",
|
||||
keywords: ["community", "governance", "decision-making", "operating manual"],
|
||||
title: homeMeta.title,
|
||||
description: homeMeta.description,
|
||||
keywords: [...homeMeta.keywords],
|
||||
authors: [{ name: "Media Economies Design Lab" }],
|
||||
creator: "Media Economies Design Lab",
|
||||
publisher: "Media Economies Design Lab",
|
||||
@@ -65,9 +66,8 @@ export const metadata: Metadata = {
|
||||
canonical: "/",
|
||||
},
|
||||
openGraph: {
|
||||
title: "CommunityRule - Build operating manuals for successful communities",
|
||||
description:
|
||||
"Help your community make important decisions in a way that reflects its unique values.",
|
||||
title: homeMeta.title,
|
||||
description: homeMeta.description,
|
||||
url: "https://communityrule.com",
|
||||
siteName: "CommunityRule",
|
||||
locale: "en_US",
|
||||
@@ -75,9 +75,8 @@ export const metadata: Metadata = {
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "CommunityRule - Build operating manuals for successful communities",
|
||||
description:
|
||||
"Help your community make important decisions in a way that reflects its unique values.",
|
||||
title: homeMeta.title,
|
||||
description: homeMeta.description,
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
|
||||
Reference in New Issue
Block a user