From 22a08ef95786230084b5c010ef6bf1bcfa3f8cb2 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 7 Apr 2026 20:11:45 -0400 Subject: [PATCH] feat: add reusable BulkInviteForm component for multi-row name+email invites Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/shared/bulk-invite-form.tsx | 131 +++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/components/shared/bulk-invite-form.tsx diff --git a/src/components/shared/bulk-invite-form.tsx b/src/components/shared/bulk-invite-form.tsx new file mode 100644 index 0000000..e6bb100 --- /dev/null +++ b/src/components/shared/bulk-invite-form.tsx @@ -0,0 +1,131 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { toast } from 'sonner' +import { Plus, Trash2, Send, Loader2 } from 'lucide-react' + +interface InviteRow { + name: string + email: string +} + +interface BulkInviteFormProps { + onSubmit: (rows: InviteRow[]) => Promise + isPending?: boolean + submitLabel?: string +} + +const emptyRow = (): InviteRow => ({ name: '', email: '' }) + +export function BulkInviteForm({ + onSubmit, + isPending = false, + submitLabel = 'Send Invites', +}: BulkInviteFormProps) { + const [rows, setRows] = useState([emptyRow()]) + + const updateRow = (index: number, field: keyof InviteRow, value: string) => { + setRows((prev) => prev.map((r, i) => (i === index ? { ...r, [field]: value } : r))) + } + + const addRow = () => setRows((prev) => [...prev, emptyRow()]) + + const removeRow = (index: number) => { + if (rows.length <= 1) return + setRows((prev) => prev.filter((_, i) => i !== index)) + } + + const validRows = rows.filter( + (r) => r.email.trim() && r.email.includes('@') + ) + + const handleSubmit = async () => { + if (validRows.length === 0) { + toast.error('Please enter at least one valid email address') + return + } + + const emails = validRows.map((r) => r.email.trim().toLowerCase()) + const dupes = emails.filter((e, i) => emails.indexOf(e) !== i) + if (dupes.length > 0) { + toast.error(`Duplicate email: ${dupes[0]}`) + return + } + + await onSubmit(validRows.map((r) => ({ name: r.name.trim(), email: r.email.trim() }))) + setRows([emptyRow()]) + } + + return ( +
+
+ {rows.map((row, index) => ( +
+
+ {index === 0 && ( + + )} + updateRow(index, 'name', e.target.value)} + disabled={isPending} + /> +
+
+ {index === 0 && ( + + )} + updateRow(index, 'email', e.target.value)} + disabled={isPending} + /> +
+ +
+ ))} +
+ +
+ + + +
+
+ ) +}