/** 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, }; }