1+ import { useState , useEffect } from 'react' ;
2+ import { BsPersonPlus } from 'react-icons/bs' ;
3+ import { MdOutlineClose } from 'react-icons/md' ;
4+
5+ import { showToast } from '../common/toast' ;
6+ import { isValidEmail } from '../../utils/validation' ;
7+ import { firestore } from '../../firebase/config' ;
8+ import { updateDoc , getDoc , doc , arrayUnion } from "@firebase/firestore" ;
9+
10+ /**
11+ * CollaboratorInviteDialog - Modal for managing document collaborators
12+ * Allows inviting new collaborators and removing existing ones
13+ */
14+ export default function CollaboratorInviteDialog ( {
15+ showInvitePopup,
16+ onClose,
17+ noteID,
18+ user
19+ } ) {
20+ const [ inputToken , setInputToken ] = useState ( "" ) ;
21+ const [ existingCollaborators , setExistingCollaborators ] = useState ( [ ] ) ;
22+ const [ documentOwner , setDocumentOwner ] = useState ( null ) ;
23+
24+ const fetchCollaborators = async ( ) => {
25+ if ( ! noteID ) return ;
26+
27+ const noteRef = doc ( firestore , 'notes' , noteID ) ;
28+ try {
29+ const documentSnapshot = await getDoc ( noteRef ) ;
30+ if ( documentSnapshot . exists ( ) ) {
31+ const data = documentSnapshot . data ( ) ;
32+ const collaborators = data . collaborators || [ ] ;
33+ const owner = data . owner || data . createdBy || data . lastModifiedBy ; // Check actual owner field first
34+
35+ setDocumentOwner ( owner || null ) ; // Explicitly handle undefined case
36+ setExistingCollaborators ( collaborators ) ;
37+ }
38+ } catch ( error ) {
39+ console . error ( 'Error fetching collaborators:' , error ) ;
40+ }
41+ } ;
42+
43+ const handleInvite = async ( email ) => {
44+ if ( ! email || ! isValidEmail ( email ) ) {
45+ return showToast . error ( "Please enter a valid email address" ) ;
46+ }
47+
48+ // Check if user is already a collaborator
49+ if ( existingCollaborators . includes ( email ) ) {
50+ return showToast . error ( "User is already a collaborator" ) ;
51+ }
52+
53+ if ( email === user ?. email ) {
54+ return showToast . error ( "You can't invite yourself" ) ;
55+ }
56+
57+ const noteRef = doc ( firestore , 'notes' , noteID ) ;
58+
59+ try {
60+ const documentSnapshot = await getDoc ( noteRef ) ;
61+ if ( documentSnapshot . exists ( ) ) {
62+ await updateDoc ( noteRef , {
63+ collaborators : arrayUnion ( email )
64+ } ) ;
65+
66+ setInputToken ( "" ) ;
67+ setExistingCollaborators ( prev => [ ...prev , email ] ) ;
68+ showToast . success ( "User invited successfully" ) ;
69+ } else {
70+ console . log ( 'Document does not exist' ) ;
71+ }
72+ } catch ( error ) {
73+ console . error ( 'Error fetching document:' , error ) ;
74+ }
75+ } ;
76+
77+ const removeCollaborator = async ( email ) => {
78+ // Prevent removing the document owner
79+ if ( email === documentOwner ) {
80+ return showToast . error ( "Cannot remove the document owner" ) ;
81+ }
82+
83+ // Only allow owner or the user themselves to remove collaborators
84+ if ( documentOwner !== user ?. email && email !== user ?. email ) {
85+ return showToast . error ( "Only the owner can remove other collaborators" ) ;
86+ }
87+
88+ const noteRef = doc ( firestore , 'notes' , noteID ) ;
89+
90+ try {
91+ const documentSnapshot = await getDoc ( noteRef ) ;
92+ if ( documentSnapshot . exists ( ) ) {
93+ const data = documentSnapshot . data ( ) ;
94+ const updatedCollaborators = ( data . collaborators || [ ] ) . filter ( collab => collab !== email ) ;
95+
96+ await updateDoc ( noteRef , {
97+ collaborators : updatedCollaborators
98+ } ) ;
99+
100+ setExistingCollaborators ( updatedCollaborators ) ;
101+ showToast . success ( email === user ?. email ? "You left the document" : "Collaborator removed" ) ;
102+ }
103+ } catch ( error ) {
104+ console . error ( 'Error removing collaborator:' , error ) ;
105+ showToast . error ( "Failed to remove collaborator" ) ;
106+ }
107+ } ;
108+
109+ // Fetch collaborators when dialog opens
110+ useEffect ( ( ) => {
111+ if ( showInvitePopup ) {
112+ fetchCollaborators ( ) ;
113+ }
114+ } , [ showInvitePopup , noteID ] ) ;
115+
116+ if ( ! showInvitePopup ) return null ;
117+
118+ return (
119+ < div className = "fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4" >
120+ < div className = "bg-white/95 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 w-full max-w-md" >
121+ { /* Header */ }
122+ < div className = "px-6 py-4 border-b border-slate-200/50" >
123+ < div className = "flex items-center justify-between" >
124+ < h3 className = "text-lg font-light text-slate-900" > Invite Collaborator</ h3 >
125+ < button
126+ onClick = { onClose }
127+ className = "p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-xl transition-all duration-300"
128+ >
129+ < MdOutlineClose className = "w-4 h-4" />
130+ </ button >
131+ </ div >
132+ </ div >
133+
134+ { /* Content */ }
135+ < div className = "px-6 py-6" >
136+ < div className = "space-y-4" >
137+ { /* Current Collaborators */ }
138+ < div >
139+ < label className = "block text-sm font-light text-slate-600 mb-3" >
140+ Current Collaborators
141+ </ label >
142+ < div className = "space-y-2 max-h-40 overflow-y-auto" >
143+ { /* Owner */ }
144+ { documentOwner ? (
145+ < div className = "flex items-center justify-between p-3 bg-slate-50/80 rounded-lg" >
146+ < div className = "flex items-center space-x-3" >
147+ < div className = "w-6 h-6 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-xs font-medium text-white" >
148+ { documentOwner . charAt ( 0 ) . toUpperCase ( ) }
149+ </ div >
150+ < div >
151+ < span className = "text-sm font-medium text-slate-900" > { documentOwner } </ span >
152+ < span className = "text-xs text-slate-500 ml-2" > (Owner)</ span >
153+ { documentOwner === user ?. email && (
154+ < span className = "text-xs text-blue-600 ml-1" > (You)</ span >
155+ ) }
156+ </ div >
157+ </ div >
158+ </ div >
159+ ) : (
160+ < div className = "flex items-center justify-between p-3 bg-amber-50/80 rounded-lg border border-amber-200/50" >
161+ < div className = "flex items-center space-x-3" >
162+ < div className = "w-6 h-6 rounded-full bg-gradient-to-br from-amber-400 to-amber-500 flex items-center justify-center text-xs font-medium text-white" >
163+ ?
164+ </ div >
165+ < div >
166+ < span className = "text-sm font-medium text-slate-900" > Unknown Owner</ span >
167+ < span className = "text-xs text-slate-500 ml-2" > (Legacy document)</ span >
168+ </ div >
169+ </ div >
170+ </ div >
171+ ) }
172+
173+ { /* Existing collaborators (excluding owner and current user to prevent duplicates) */ }
174+ { existingCollaborators
175+ . filter ( email => email !== documentOwner ) // Don't show owner twice
176+ . map ( ( email , index ) => (
177+ < div key = { email } className = "flex items-center justify-between p-3 bg-slate-50/80 rounded-lg" >
178+ < div className = "flex items-center space-x-3" >
179+ < div className = "w-6 h-6 rounded-full bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center text-xs font-medium text-white" >
180+ { email . charAt ( 0 ) . toUpperCase ( ) }
181+ </ div >
182+ < div >
183+ < span className = "text-sm text-slate-900" > { email } </ span >
184+ { email === user ?. email && (
185+ < span className = "text-xs text-blue-600 ml-2" > (You)</ span >
186+ ) }
187+ </ div >
188+ </ div >
189+ { ( documentOwner === user ?. email || email === user ?. email ) && (
190+ < button
191+ onClick = { ( ) => removeCollaborator ( email ) }
192+ className = "text-red-500 hover:text-red-700 hover:bg-red-50 p-1 rounded-lg transition-all duration-200"
193+ title = { email === user ?. email ? "Leave document" : "Remove collaborator" }
194+ >
195+ < MdOutlineClose className = "w-4 h-4" />
196+ </ button >
197+ ) }
198+ </ div >
199+ ) )
200+ }
201+
202+ { existingCollaborators . filter ( email => email !== documentOwner ) . length === 0 && ! documentOwner && (
203+ < div className = "text-center py-4 text-slate-400" >
204+ < span className = "text-sm" > No collaborators yet</ span >
205+ </ div >
206+ ) }
207+ </ div >
208+ </ div >
209+
210+ { /* Add new collaborator */ }
211+ < div className = "border-t border-slate-200/50 pt-4" >
212+ < label className = "block text-sm font-light text-slate-600 mb-2" >
213+ Invite New Collaborator
214+ </ label >
215+ < input
216+ type = "text"
217+ placeholder = "example@outlook.com"
218+ value = { inputToken }
219+ onChange = { ( e ) => setInputToken ( e . target . value ) }
220+ className = "w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-slate-900 placeholder-slate-400 focus:outline-none focus:border-slate-400 focus:bg-white transition-all duration-300 font-light"
221+ onKeyPress = { ( e ) => {
222+ if ( e . key === 'Enter' ) {
223+ handleInvite ( inputToken ) ;
224+ }
225+ } }
226+ autoFocus
227+ />
228+ </ div >
229+
230+ < div className = "flex items-center space-x-3 pt-2" >
231+ < button
232+ onClick = { onClose }
233+ className = "flex-1 px-4 py-3 text-slate-600 hover:text-slate-900 hover:bg-slate-100 rounded-xl transition-all duration-300 font-light"
234+ >
235+ Done
236+ </ button >
237+ < button
238+ onClick = { ( ) => handleInvite ( inputToken ) }
239+ disabled = { ! inputToken . trim ( ) }
240+ className = "flex-1 px-4 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed font-light flex items-center justify-center space-x-2"
241+ >
242+ < BsPersonPlus className = "w-4 h-4" />
243+ < span > Invite</ span >
244+ </ button >
245+ </ div >
246+ </ div >
247+ </ div >
248+ </ div >
249+ </ div >
250+ ) ;
251+ }
0 commit comments