'use client' import { useState, useCallback, useMemo } from 'react' import Link from 'next/link' import { useRouter } from 'next/navigation' import Papa from 'papaparse' import { trpc } from '@/lib/trpc/client' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Badge } from '@/components/ui/badge' import { Progress } from '@/components/ui/progress' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { ArrowLeft, ArrowRight, AlertCircle, CheckCircle2, Loader2, Users, X, Mail, FileSpreadsheet, } from 'lucide-react' import { cn } from '@/lib/utils' type Step = 'input' | 'preview' | 'sending' | 'complete' type Role = 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER' interface ParsedUser { email: string name?: string isValid: boolean error?: string isDuplicate?: boolean } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ export default function MemberInvitePage() { const router = useRouter() const [step, setStep] = useState('input') const [inputMethod, setInputMethod] = useState<'textarea' | 'csv'>('textarea') const [emailsText, setEmailsText] = useState('') const [csvFile, setCsvFile] = useState(null) const [role, setRole] = useState('JURY_MEMBER') const [expertiseTags, setExpertiseTags] = useState([]) const [tagInput, setTagInput] = useState('') const [parsedUsers, setParsedUsers] = useState([]) const [sendProgress, setSendProgress] = useState(0) const [result, setResult] = useState<{ created: number; skipped: number } | null>(null) const bulkCreate = trpc.user.bulkCreate.useMutation() const parseEmailsFromText = useCallback((text: string): ParsedUser[] => { const lines = text.split(/[\n,;]+/).map((line) => line.trim()).filter(Boolean) const seenEmails = new Set() return lines.map((line) => { const matchWithName = line.match(/^(.+?)\s*<(.+?)>$/) const email = matchWithName ? matchWithName[2].trim().toLowerCase() : line.toLowerCase() const name = matchWithName ? matchWithName[1].trim() : undefined const isValidFormat = emailRegex.test(email) const isDuplicate = seenEmails.has(email) if (isValidFormat && !isDuplicate) seenEmails.add(email) return { email, name, isValid: isValidFormat && !isDuplicate, isDuplicate, error: !isValidFormat ? 'Invalid email format' : isDuplicate ? 'Duplicate email' : undefined, } }) }, []) const handleCSVUpload = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return setCsvFile(file) Papa.parse>(file, { header: true, skipEmptyLines: true, complete: (results) => { const seenEmails = new Set() const users: ParsedUser[] = results.data.map((row) => { const emailKey = Object.keys(row).find((key) => key.toLowerCase() === 'email' || key.toLowerCase().includes('email')) const nameKey = Object.keys(row).find((key) => key.toLowerCase() === 'name' || key.toLowerCase().includes('name')) const email = emailKey ? row[emailKey]?.trim().toLowerCase() : '' const name = nameKey ? row[nameKey]?.trim() : undefined const isValidFormat = emailRegex.test(email) const isDuplicate = email ? seenEmails.has(email) : false if (isValidFormat && !isDuplicate && email) seenEmails.add(email) return { email, name, isValid: isValidFormat && !isDuplicate, isDuplicate, error: !email ? 'No email found' : !isValidFormat ? 'Invalid email format' : isDuplicate ? 'Duplicate email' : undefined, } }) setParsedUsers(users.filter((u) => u.email)) setStep('preview') }, }) }, []) const handleTextProceed = () => { setParsedUsers(parseEmailsFromText(emailsText)); setStep('preview') } const addTag = () => { const tag = tagInput.trim(); if (tag && !expertiseTags.includes(tag)) { setExpertiseTags([...expertiseTags, tag]); setTagInput('') } } const removeTag = (tag: string) => setExpertiseTags(expertiseTags.filter((t) => t !== tag)) const summary = useMemo(() => { const validUsers = parsedUsers.filter((u) => u.isValid) const invalidUsers = parsedUsers.filter((u) => !u.isValid) const duplicateUsers = parsedUsers.filter((u) => u.isDuplicate) return { total: parsedUsers.length, valid: validUsers.length, invalid: invalidUsers.length, duplicates: duplicateUsers.length, validUsers, invalidUsers, duplicateUsers } }, [parsedUsers]) const removeInvalidUsers = () => setParsedUsers(parsedUsers.filter((u) => u.isValid)) const handleSendInvites = async () => { if (summary.valid === 0) return setStep('sending'); setSendProgress(0) try { const result = await bulkCreate.mutateAsync({ users: summary.validUsers.map((u) => ({ email: u.email, name: u.name, role, expertiseTags: expertiseTags.length > 0 ? expertiseTags : undefined })), }) setSendProgress(100); setResult(result); setStep('complete') } catch { setStep('preview') } } const resetForm = () => { setStep('input'); setEmailsText(''); setCsvFile(null); setParsedUsers([]); setResult(null); setSendProgress(0) } const steps: Array<{ key: Step; label: string }> = [ { key: 'input', label: 'Input' }, { key: 'preview', label: 'Preview' }, { key: 'sending', label: 'Send' }, { key: 'complete', label: 'Done' }, ] const currentStepIndex = steps.findIndex((s) => s.key === step) const renderStep = () => { switch (step) { case 'input': return ( Invite Members Add email addresses to invite new members to the platform
{inputMethod === 'textarea' ? (