Invite people after review, then finish the Rule, with login before inviting and Invite your members on Profile.

This commit is contained in:
adilallo committed 2026-11-06 08:56:13 -07:00
1 parent 88bfccd1e7
commit 70ceb73104
31 files changed
+1388 -401

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
import type { CreateFlowState } from "../../app/(app)/create/types";
import { RULE_SECTION_CATEGORY } from "./ruleSectionsFromMethodSelections";
import {
membershipPresetFor,
publishedMethodDisplayLabel,
} from "./finalReviewChipPresets";
export type MembershipInvitePreview = {
categoryName: string;
title: string;
eligibility: string;
joiningProcess: string;
};
/** First selected membership method, for the Invite your members card. */
export function membershipInvitePreview(
state: CreateFlowState,
): MembershipInvitePreview | null {
const ids = state.selectedMembershipMethodIds ?? [];
const firstId = ids.find((id) => typeof id === "string" && id.trim() !== "");
if (!firstId) return null;
const saved = state.membershipMethodDetailsById?.[firstId];
const preset = membershipPresetFor(firstId);
const eligibility = (saved?.eligibility ?? "").trim() || preset.eligibility;
const joiningProcess =
(saved?.joiningProcess ?? "").trim() || preset.joiningProcess;
const title =
publishedMethodDisplayLabel(
"membership",
firstId,
state.customMethodCardMetaById,
) || firstId;
if (!title && !eligibility && !joiningProcess) return null;
return {
categoryName: RULE_SECTION_CATEGORY.membership,
title,
eligibility,
joiningProcess,
};
}
+82
View File
@@ -0,0 +1,82 @@
/** Shared invite-field email check (create-flow Invite your members). */
export const INVITE_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const COUNT_WORDS = [
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten",
] as const;
/** Lowercase count for paste-result copy (`five`, or digits past ten). */
export function formatInviteCountWord(n: number): string {
if (Number.isInteger(n) && n >= 0 && n <= 10) {
return COUNT_WORDS[n] ?? String(n);
}
return String(n);
}
export function splitInvitePasteTokens(raw: string): string[] {
return raw
.split(/[\s,;]+/)
.map((token) => token.trim())
.filter((token) => token.length > 0);
}
export type ParseInvitePasteResult = {
added: string[];
invalidRemainder: string;
skippedDuplicate: number;
};
/**
* Split a pasted list into valid new addresses and leftovers that stay in the
* field. Tokens that are not emails, duplicates, or that would exceed `max`
* remain in {@link ParseInvitePasteResult.invalidRemainder}.
*/
export function parseInvitePaste(
raw: string,
existing: readonly string[],
max: number,
): ParseInvitePasteResult {
const tokens = splitInvitePasteTokens(raw);
const added: string[] = [];
const invalid: string[] = [];
const seen = new Set(
existing
.map((email) => email.trim().toLowerCase())
.filter((email) => email.length > 0),
);
let skippedDuplicate = 0;
for (const token of tokens) {
const lower = token.toLowerCase();
if (!INVITE_EMAIL_PATTERN.test(lower)) {
invalid.push(token);
continue;
}
if (seen.has(lower)) {
skippedDuplicate += 1;
continue;
}
if (existing.length + added.length >= max) {
invalid.push(token);
continue;
}
seen.add(lower);
added.push(lower);
}
return {
added,
invalidRemainder: invalid.join(", "),
skippedDuplicate,
};
}