From 5beeb377203857f7521ac6fc2d57ab7a749cb724 Mon Sep 17 00:00:00 2001 From: Stella112 Date: Sun, 28 Jun 2026 16:19:05 +0100 Subject: [PATCH] Guard manual member additions Closes JointSave-org/Joint_Save#144 --- .../components/create-group/BulkImport.tsx | 12 +++++----- .../components/create-group/flexible-form.tsx | 22 +++++++++++++++++-- .../create-group/rotational-form.tsx | 17 +++++++++++++- .../components/create-group/target-form.tsx | 22 +++++++++++++++++-- frontend/lib/constants.ts | 1 + 5 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 frontend/lib/constants.ts diff --git a/frontend/components/create-group/BulkImport.tsx b/frontend/components/create-group/BulkImport.tsx index 8288205..ab0f04f 100644 --- a/frontend/components/create-group/BulkImport.tsx +++ b/frontend/components/create-group/BulkImport.tsx @@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input"; import { AlertCircle, X } from "lucide-react"; import Papa from "papaparse"; import { isValidStellarAddress } from "@/utils/stellarAddress"; +import { MAX_POOL_MEMBERS } from "@/lib/constants"; type Member = { address: string; @@ -24,8 +25,6 @@ export default function BulkImport({ onMembersChange }: BulkImportProps) { const [members, setMembers] = useState([]); const [errors, setErrors] = useState([]); - const MAX_MEMBERS = 50; - const handleFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; @@ -49,12 +48,13 @@ export default function BulkImport({ onMembersChange }: BulkImportProps) { } parsed.push({ address, name, line: lineNum }); }); - if (parsed.length > MAX_MEMBERS) { - errorLines.push(`Too many members (${parsed.length}). The maximum allowed is ${MAX_MEMBERS}.`); + if (parsed.length > MAX_POOL_MEMBERS) { + errorLines.push(`Too many members (${parsed.length}). The maximum allowed is ${MAX_POOL_MEMBERS}.`); } - setMembers(parsed); + const acceptedMembers = parsed.slice(0, MAX_POOL_MEMBERS); + setMembers(acceptedMembers); setErrors(errorLines); - onMembersChange(parsed.map((m) => m.address)); + onMembersChange(acceptedMembers.map((m) => m.address)); }, error: (err) => { setErrors([`Parsing error: ${err.message}`]); diff --git a/frontend/components/create-group/flexible-form.tsx b/frontend/components/create-group/flexible-form.tsx index d9a4faf..fc1172a 100644 --- a/frontend/components/create-group/flexible-form.tsx +++ b/frontend/components/create-group/flexible-form.tsx @@ -20,6 +20,7 @@ import { validatePositiveAmount, validateWithdrawalFee, } from "@/lib/form-validation" +import { MAX_POOL_MEMBERS } from "@/lib/constants" function isValidStellarAddress(addr: string) { return /^G[A-Z2-7]{55}$/.test(addr) @@ -56,6 +57,7 @@ export function FlexibleForm() { const allMembers = address ? [address, ...members] : members const validMembers = Array.from(new Set(allMembers.filter(isValidStellarAddress))) const isCreating = step !== "idle" + const isMemberLimitReached = members.length >= MAX_POOL_MEMBERS const validateField = useCallback((name: keyof FieldErrors, value: string) => { let message = "" @@ -77,7 +79,11 @@ export function FlexibleForm() { setMemberErrors(errs) } - const addMember = () => { setMembers([...members, ""]); setMemberErrors([...memberErrors, ""]) } + const addMember = () => { + if (isMemberLimitReached) return + setMembers([...members, ""]) + setMemberErrors([...memberErrors, ""]) + } const removeMember = (i: number) => { setMembers(members.filter((_, idx) => idx !== i)) setMemberErrors(memberErrors.filter((_, idx) => idx !== i)) @@ -313,10 +319,22 @@ export function FlexibleForm() { tooltip="Add the public Stellar address (starts with G) for each person joining this pool. You are automatically included." required /> - + {isMemberLimitReached && ( +

+ Maximum of {MAX_POOL_MEMBERS} members reached +

+ )}
diff --git a/frontend/components/create-group/rotational-form.tsx b/frontend/components/create-group/rotational-form.tsx index 5f5d903..4eca8f0 100644 --- a/frontend/components/create-group/rotational-form.tsx +++ b/frontend/components/create-group/rotational-form.tsx @@ -21,6 +21,7 @@ import { validatePositiveAmount, } from "@/lib/form-validation" import type { DuplicatePrefill } from "@/app/dashboard/create/[type]/page" +import { MAX_POOL_MEMBERS } from "@/lib/constants" function isValidStellarAddress(addr: string) { return /^G[A-Z2-7]{55}$/.test(addr) @@ -76,6 +77,7 @@ export function RotationalForm({ prefill }: { prefill?: DuplicatePrefill }) { const allMembers = address ? [address, ...members] : members const validMembers = Array.from(new Set(allMembers.filter(isValidStellarAddress))) const isCreating = step !== "idle" + const isMemberLimitReached = members.length >= MAX_POOL_MEMBERS const validateField = useCallback((name: keyof FieldErrors, value: string) => { const result = @@ -103,6 +105,7 @@ export function RotationalForm({ prefill }: { prefill?: DuplicatePrefill }) { } const addMember = () => { + if (isMemberLimitReached) return setMembers([...members, ""]) setMemberErrors([...memberErrors, ""]) } @@ -330,10 +333,22 @@ export function RotationalForm({ prefill }: { prefill?: DuplicatePrefill }) { tooltip="Add the public Stellar address (starts with G) for each person joining this pool. You are automatically included as the first member." required /> -
+ {isMemberLimitReached && ( +

+ Maximum of {MAX_POOL_MEMBERS} members reached +

+ )}
{/* Creator — always included, read-only */} diff --git a/frontend/components/create-group/target-form.tsx b/frontend/components/create-group/target-form.tsx index d31318d..a3e16ba 100644 --- a/frontend/components/create-group/target-form.tsx +++ b/frontend/components/create-group/target-form.tsx @@ -20,6 +20,7 @@ import { validatePositiveAmount, validateDeadline, } from "@/lib/form-validation" +import { MAX_POOL_MEMBERS } from "@/lib/constants" function isValidStellarAddress(addr: string) { return /^G[A-Z2-7]{55}$/.test(addr) @@ -62,6 +63,7 @@ export function TargetForm() { const allMembers = address ? [address, ...members] : members const validMembers = Array.from(new Set(allMembers.filter(isValidStellarAddress))) const isCreating = step !== "idle" + const isMemberLimitReached = members.length >= MAX_POOL_MEMBERS const validateField = useCallback((name: keyof FieldErrors, value: string) => { let message = "" @@ -83,7 +85,11 @@ export function TargetForm() { setMemberErrors(errs) } - const addMember = () => { setMembers([...members, ""]); setMemberErrors([...memberErrors, ""]) } + const addMember = () => { + if (isMemberLimitReached) return + setMembers([...members, ""]) + setMemberErrors([...memberErrors, ""]) + } const removeMember = (i: number) => { setMembers(members.filter((_, idx) => idx !== i)) setMemberErrors(memberErrors.filter((_, idx) => idx !== i)) @@ -297,10 +303,22 @@ export function TargetForm() { tooltip="Add the public Stellar address (starts with G) for each person joining this pool. You are automatically included." required /> -
+ {isMemberLimitReached && ( +

+ Maximum of {MAX_POOL_MEMBERS} members reached +

+ )}
diff --git a/frontend/lib/constants.ts b/frontend/lib/constants.ts new file mode 100644 index 0000000..e917281 --- /dev/null +++ b/frontend/lib/constants.ts @@ -0,0 +1 @@ +export const MAX_POOL_MEMBERS = 50