11'use client'
22
3- import React , { useState } from 'react'
3+ import React from 'react'
44import { ChevronDown } from 'lucide-react'
55import {
66 Button ,
77 ButtonGroup ,
88 ButtonGroupItem ,
99 Checkbox ,
10- Input ,
1110 Popover ,
1211 PopoverContent ,
1312 PopoverItem ,
1413 PopoverTrigger ,
14+ TagInput ,
15+ type TagItem ,
1516} from '@/components/emcn'
1617import { cn } from '@/lib/core/utils/cn'
1718import { quickValidateEmail } from '@/lib/messaging/email/validation'
@@ -64,8 +65,8 @@ const PermissionSelector = React.memo<PermissionSelectorProps>(
6465PermissionSelector . displayName = 'PermissionSelector'
6566
6667interface MemberInvitationCardProps {
67- inviteEmail : string
68- setInviteEmail : ( email : string ) => void
68+ inviteEmails : TagItem [ ]
69+ setInviteEmails : ( emails : TagItem [ ] ) => void
6970 isInviting : boolean
7071 showWorkspaceInvite : boolean
7172 setShowWorkspaceInvite : ( show : boolean ) => void
@@ -82,8 +83,8 @@ interface MemberInvitationCardProps {
8283}
8384
8485export function MemberInvitationCard ( {
85- inviteEmail ,
86- setInviteEmail ,
86+ inviteEmails ,
87+ setInviteEmails ,
8788 isInviting,
8889 showWorkspaceInvite,
8990 setShowWorkspaceInvite,
@@ -100,45 +101,26 @@ export function MemberInvitationCard({
100101} : MemberInvitationCardProps ) {
101102 const selectedCount = selectedWorkspaces . length
102103 const hasAvailableSeats = availableSeats > 0
103- const [ emailError , setEmailError ] = useState < string > ( '' )
104+ const hasValidEmails = inviteEmails . some ( ( e ) => e . isValid )
104105
105- const validateEmailInput = ( email : string ) => {
106- if ( ! email . trim ( ) ) {
107- setEmailError ( '' )
108- return
109- }
106+ const handleAddEmail = ( value : string ) => {
107+ const normalized = value . trim ( ) . toLowerCase ( )
108+ if ( ! normalized ) return false
110109
111- const validation = quickValidateEmail ( email . trim ( ) )
112- if ( ! validation . isValid ) {
113- setEmailError ( validation . reason || 'Please enter a valid email address' )
114- } else {
115- setEmailError ( '' )
116- }
117- }
110+ const isDuplicate = inviteEmails . some ( ( e ) => e . value === normalized )
111+ if ( isDuplicate ) return false
118112
119- const handleEmailChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
120- const value = e . target . value
121- setInviteEmail ( value )
122- if ( emailError ) {
123- setEmailError ( '' )
124- }
113+ const validation = quickValidateEmail ( normalized )
114+ setInviteEmails ( [ ...inviteEmails , { value : normalized , isValid : validation . isValid } ] )
115+ return validation . isValid
125116 }
126117
127- const handleInviteClick = ( ) => {
128- if ( inviteEmail . trim ( ) ) {
129- validateEmailInput ( inviteEmail )
130- const validation = quickValidateEmail ( inviteEmail . trim ( ) )
131- if ( ! validation . isValid ) {
132- return // Don't proceed if validation fails
133- }
134- }
135-
136- onInviteMember ( )
118+ const handleRemoveEmail = ( _value : string , index : number ) => {
119+ setInviteEmails ( inviteEmails . filter ( ( _ , i ) => i !== index ) )
137120 }
138121
139122 return (
140123 < div className = 'overflow-hidden rounded-[6px] border border-[var(--border-1)] bg-[var(--surface-5)]' >
141- { /* Header */ }
142124 < div className = 'px-[14px] py-[10px]' >
143125 < h4 className = 'font-medium text-[14px] text-[var(--text-primary)]' > Invite Team Members</ h4 >
144126 < p className = 'text-[12px] text-[var(--text-muted)]' >
@@ -147,46 +129,18 @@ export function MemberInvitationCard({
147129 </ div >
148130
149131 < div className = 'flex flex-col gap-[12px] border-[var(--border-1)] border-t bg-[var(--surface-4)] px-[14px] py-[12px]' >
150- { /* Main invitation input */ }
151132 < div className = 'flex items-start gap-[8px]' >
152133 < div className = 'flex-1' >
153- { /* Hidden decoy fields to prevent browser autofill */ }
154- < input
155- type = 'text'
156- name = 'fakeusernameremembered'
157- autoComplete = 'username'
158- style = { { position : 'absolute' , left : '-9999px' , opacity : 0 , pointerEvents : 'none' } }
159- tabIndex = { - 1 }
160- readOnly
161- />
162- < input
163- type = 'email'
164- name = 'fakeemailremembered'
165- autoComplete = 'email'
166- style = { { position : 'absolute' , left : '-9999px' , opacity : 0 , pointerEvents : 'none' } }
167- tabIndex = { - 1 }
168- readOnly
169- />
170- < Input
171- placeholder = 'Enter email address'
172- value = { inviteEmail }
173- onChange = { handleEmailChange }
134+ < TagInput
135+ items = { inviteEmails }
136+ onAdd = { handleAddEmail }
137+ onRemove = { handleRemoveEmail }
138+ placeholder = 'Enter email addresses'
139+ placeholderWithTags = 'Add another email'
174140 disabled = { isInviting || ! hasAvailableSeats }
175- className = { cn ( emailError && 'border-red-500 focus-visible:ring-red-500' ) }
176- name = 'member_invite_field'
177- autoComplete = 'off'
178- autoCorrect = 'off'
179- autoCapitalize = 'off'
180- spellCheck = { false }
181- data-lpignore = 'true'
182- data-form-type = 'other'
183- aria-autocomplete = 'none'
141+ triggerKeys = { [ 'Enter' , ',' , ' ' ] }
142+ maxHeight = 'max-h-24'
184143 />
185- { emailError && (
186- < p className = 'mt-1 text-[12px] text-[var(--text-error)] leading-tight' >
187- { emailError }
188- </ p >
189- ) }
190144 </ div >
191145 < Popover
192146 open = { showWorkspaceInvite }
@@ -287,14 +241,13 @@ export function MemberInvitationCard({
287241 </ Popover >
288242 < Button
289243 variant = 'tertiary'
290- onClick = { handleInviteClick }
291- disabled = { ! inviteEmail || isInviting || ! hasAvailableSeats }
244+ onClick = { ( ) => onInviteMember ( ) }
245+ disabled = { ! hasValidEmails || isInviting || ! hasAvailableSeats }
292246 >
293247 { isInviting ? 'Inviting...' : hasAvailableSeats ? 'Invite' : 'No Seats' }
294248 </ Button >
295249 </ div >
296250
297- { /* Invitation error - inline */ }
298251 { invitationError && (
299252 < p className = 'text-[12px] text-[var(--text-error)] leading-tight' >
300253 { invitationError instanceof Error && invitationError . message
@@ -303,7 +256,6 @@ export function MemberInvitationCard({
303256 </ p >
304257 ) }
305258
306- { /* Success message */ }
307259 { inviteSuccess && (
308260 < p className = 'text-[11px] text-[var(--text-success)] leading-tight' >
309261 Invitation sent successfully
0 commit comments