From 3c984e00c52c36518b338460edba2c085fc9f70c Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Fri, 6 Nov 2026 09:16:06 -0700 Subject: [PATCH] Show invited people the Rule, let them copy its cards for their community, and replace an expired invitation link. --- app/(app)/create/CreateFlowLayoutClient.tsx | 18 +- .../screens/completed/CompletedScreen.tsx | 67 ++++- app/(app)/invite/[ruleId]/page.tsx | 50 ++++ .../_components/ExpiredInviteScreen.tsx | 149 ++++++++++ .../invite/_components/InvitedRuleView.tsx | 276 ++++++++++++++++++ .../invite/_components/MakeACopyDialog.tsx | 122 ++++++++ app/(app)/invite/expired/layout.tsx | 18 ++ app/(app)/invite/expired/page.tsx | 24 ++ app/(app)/invite/layout.tsx | 14 + .../profile/_components/ProfilePage.view.tsx | 2 +- .../invites/rule-stakeholder/resend/route.ts | 138 +++++++++ .../invites/rule-stakeholder/verify/route.ts | 12 +- app/api/rules/[id]/make-copy/route.ts | 105 +++++++ lib/create/api.ts | 113 +++++++ lib/create/copiedRuleDocument.ts | 59 ++++ lib/create/inviteLastChanged.ts | 10 + lib/create/invitePaths.ts | 20 ++ lib/create/lastPublishedRule.ts | 29 +- lib/create/ruleInviteCardMeta.ts | 22 +- lib/navigationChromelessPath.ts | 6 +- lib/server/publishedRules.ts | 57 ++++ lib/server/validation/createFlowSchemas.ts | 19 ++ .../create/reviewAndComplete/completed.json | 4 + messages/en/index.ts | 2 + messages/en/metadata.json | 8 + messages/en/pages/invite.json | 50 ++++ tests/components/CompletedPage.test.tsx | 18 ++ tests/components/ExpiredInviteScreen.test.tsx | 63 ++++ tests/components/InvitedRuleView.test.tsx | 68 +++++ tests/components/TextPage.test.tsx | 2 +- tests/unit/copiedRuleDocument.test.ts | 78 +++++ tests/unit/invitePaths.test.ts | 27 ++ .../unit/lib/navigationChromelessPath.test.ts | 2 + tests/unit/ruleInviteCardMeta.test.ts | 24 ++ ...takeholderInviteResendExpiredRoute.test.ts | 112 +++++++ .../ruleStakeholderInviteVerifyRoute.test.ts | 48 ++- tests/unit/rulesMakeCopyRoute.test.ts | 145 +++++++++ 37 files changed, 1962 insertions(+), 19 deletions(-) create mode 100644 app/(app)/invite/[ruleId]/page.tsx create mode 100644 app/(app)/invite/_components/ExpiredInviteScreen.tsx create mode 100644 app/(app)/invite/_components/InvitedRuleView.tsx create mode 100644 app/(app)/invite/_components/MakeACopyDialog.tsx create mode 100644 app/(app)/invite/expired/layout.tsx create mode 100644 app/(app)/invite/expired/page.tsx create mode 100644 app/(app)/invite/layout.tsx create mode 100644 app/api/invites/rule-stakeholder/resend/route.ts create mode 100644 app/api/rules/[id]/make-copy/route.ts create mode 100644 lib/create/copiedRuleDocument.ts create mode 100644 lib/create/inviteLastChanged.ts create mode 100644 lib/create/invitePaths.ts create mode 100644 messages/en/pages/invite.json create mode 100644 tests/components/ExpiredInviteScreen.test.tsx create mode 100644 tests/components/InvitedRuleView.test.tsx create mode 100644 tests/unit/copiedRuleDocument.test.ts create mode 100644 tests/unit/invitePaths.test.ts create mode 100644 tests/unit/ruleStakeholderInviteResendExpiredRoute.test.ts create mode 100644 tests/unit/rulesMakeCopyRoute.test.ts diff --git a/app/(app)/create/CreateFlowLayoutClient.tsx b/app/(app)/create/CreateFlowLayoutClient.tsx index 9e46c6b..33ce2ab 100644 --- a/app/(app)/create/CreateFlowLayoutClient.tsx +++ b/app/(app)/create/CreateFlowLayoutClient.tsx @@ -62,6 +62,10 @@ import { } from "../../../lib/create/publishedDocumentToCreateFlowState"; import { METHOD_FACET_API_SECTION_IDS } from "../../../lib/create/customRuleFacets"; import { readLastPublishedRule } from "../../../lib/create/lastPublishedRule"; +import { + CREATE_FLOW_COPIED_QUERY, + CREATE_FLOW_COPIED_VALUE, +} from "../../../lib/create/invitePaths"; import { clearPendingKeepRuleLogin, hasPendingKeepRuleLogin, @@ -220,6 +224,7 @@ function CreateFlowLayoutContent({ const [leaveConfirmOpen, setLeaveConfirmOpen] = useState(false); const [completedHasPublishedRule, setCompletedHasPublishedRule] = useState(false); + const [completedCopiedReady, setCompletedCopiedReady] = useState(false); type LeaveConfirmChoice = "leave" | "save" | "cancel"; const leaveConfirmResolverRef = useRef< ((choice: LeaveConfirmChoice) => void) | null @@ -548,9 +553,12 @@ function CreateFlowLayoutContent({ if (currentStep !== "completed") { setCompletedFlowBanner(null); setCompletedHasPublishedRule(false); + setCompletedCopiedReady(false); return; } - setCompletedHasPublishedRule(Boolean(readLastPublishedRule())); + const last = readLastPublishedRule(); + setCompletedHasPublishedRule(Boolean(last)); + setCompletedCopiedReady(Boolean(last?.copiedFromTitle)); }, [currentStep]); useEffect(() => { @@ -913,7 +921,13 @@ function CreateFlowLayoutContent({ onExit={(opts) => void handleExit(opts)} buttonPalette={isCompletedStep ? "inverse" : undefined} className={`relative z-[210] shrink-0 ${ - isCompletedStep ? "!bg-[var(--color-teal-teal50,#c9fef9)]" : "" + isCompletedStep + ? completedCopiedReady || + searchParams.get(CREATE_FLOW_COPIED_QUERY) === + CREATE_FLOW_COPIED_VALUE + ? "!bg-[var(--color-lavender-lavender150,#d1bdf3)]" + : "!bg-[var(--color-teal-teal50,#c9fef9)]" + : "" }`.trim()} /> {currentStep === "community-save" ? ( diff --git a/app/(app)/create/screens/completed/CompletedScreen.tsx b/app/(app)/create/screens/completed/CompletedScreen.tsx index f85dd25..9e86423 100644 --- a/app/(app)/create/screens/completed/CompletedScreen.tsx +++ b/app/(app)/create/screens/completed/CompletedScreen.tsx @@ -26,6 +26,11 @@ import { CREATE_FLOW_COMPLETED_CELEBRATE_QUERY, CREATE_FLOW_COMPLETED_CELEBRATE_VALUE, } from "../../utils/flowSteps"; +import { + CREATE_FLOW_COPIED_QUERY, + CREATE_FLOW_COPIED_VALUE, +} from "../../../../../lib/create/invitePaths"; +import { communityPossessive } from "../../../../../lib/create/ruleInviteCardMeta"; function emptyCompletedDocumentState(): { headerTitle: string; @@ -80,6 +85,11 @@ export function CompletedScreen() { const celebrateInUrl = searchParams.get(CREATE_FLOW_COMPLETED_CELEBRATE_QUERY) === CREATE_FLOW_COMPLETED_CELEBRATE_VALUE; + const copiedInUrl = + searchParams.get(CREATE_FLOW_COPIED_QUERY) === CREATE_FLOW_COPIED_VALUE; + const storedForCopy = ruleIdParam ? null : readLastPublishedRule(); + const copiedFromTitle = storedForCopy?.copiedFromTitle; + const copiedNameHit = storedForCopy?.copiedNameHit ?? null; const mdUp = useCreateFlowMdUp(); const m = useMessages(); const completed = m.create.reviewAndComplete.completed; @@ -88,6 +98,7 @@ export function CompletedScreen() { const [toastDismissed, setToastDismissed] = useState(false); /** Latch: toast copy should survive `router.replace` after we strip `?celebrate=1`. */ const [showCelebrateToast] = useState(celebrateInUrl); + const [showCopiedReady] = useState(copiedInUrl); const [askOrganizerOpen, setAskOrganizerOpen] = useState(false); const [headerTitle, setHeaderTitle] = useState(initial.headerTitle); const [headerDescription, setHeaderDescription] = useState< @@ -141,11 +152,17 @@ export function CompletedScreen() { }, [ruleIdParam, router]); useEffect(() => { - if (!celebrateInUrl) return; + if (!celebrateInUrl && !copiedInUrl) return; router.replace("/create/completed"); - }, [celebrateInUrl, router]); + }, [celebrateInUrl, copiedInUrl, router]); - const toast = showCelebrateToast && !toastDismissed ? ( + const surfaceCopied = showCopiedReady || Boolean(copiedFromTitle); + const surfaceClass = surfaceCopied + ? "bg-[var(--color-lavender-lavender150,#d1bdf3)]" + : "bg-[var(--color-teal-teal50,#c9fef9)]"; + + const toast = + !toastDismissed && (showCopiedReady || showCelebrateToast) ? (
setToastDismissed(true)} @@ -166,7 +194,7 @@ export function CompletedScreen() { return ( <> -
+
@@ -180,7 +208,7 @@ export function CompletedScreen() { size="L" palette="inverse" /> - {headerTitle ? ( + {headerTitle && !surfaceCopied ? (

{completed.askOrganizerLead}{" "}