1- import { useState } from "react" ;
1+ import { useState , useMemo } from "react" ;
22import type { ReactNode } from "react" ;
3- import { AlertCircle , ChevronRight , Copy , Folder , Globe2 , Link2 , Lock , Play , Trash2 , X } from "lucide-react" ;
3+ import { AlertCircle , ChevronRight , Copy , Folder , Github , Globe2 , Link2 , Lock , Mail , Play , Trash2 , X } from "lucide-react" ;
44import { AnimatePresence , motion } from "motion/react" ;
55
66interface TeamInviteModalProps {
77 isOpen : boolean ;
88 onClose : ( ) => void ;
99}
1010
11+ const CURRENT_USER_ID = "junwoo" ;
12+
1113interface TeamMember {
1214 id : string ;
1315 name : string ;
1416 avatar : string ;
1517 role : "owner" | "editor" | "viewer" ;
1618 warning ?: boolean ;
19+ isGithub ?: boolean ;
1720}
1821
1922const initialMembers : TeamMember [ ] = [
23+ { id : "junwoo" , name : "김준우" , avatar : "준" , role : "owner" } ,
2024 { id : "jaejun" , name : "김재준" , avatar : "재" , role : "editor" } ,
2125 { id : "jinpil" , name : "김진필" , avatar : "필" , role : "editor" } ,
22- { id : "junwoo" , name : "김준우" , avatar : "준" , role : "owner" } ,
2326 { id : "jinhyun" , name : "김진현" , avatar : "현" , role : "editor" , warning : true } ,
2427 { id : "hyun" , name : "안현" , avatar : "안" , role : "editor" , warning : true }
2528] ;
@@ -32,27 +35,36 @@ const permissionLabels = {
3235
3336export function TeamInviteModal ( { isOpen, onClose } : TeamInviteModalProps ) {
3437 const [ inviteValue , setInviteValue ] = useState ( "" ) ;
38+ const [ inviteMode , setInviteMode ] = useState < "email" | "github" > ( "email" ) ;
3539 const [ members , setMembers ] = useState ( initialMembers ) ;
3640 const [ copyState , setCopyState ] = useState < "idle" | "copied" > ( "idle" ) ;
3741 const [ confirmTarget , setConfirmTarget ] = useState < TeamMember | null > ( null ) ;
3842
43+ // Current user always first, rest follow in original order
44+ const sortedMembers = useMemo ( ( ) => {
45+ const me = members . find ( ( m ) => m . id === CURRENT_USER_ID ) ;
46+ const others = members . filter ( ( m ) => m . id !== CURRENT_USER_ID ) ;
47+ return me ? [ me , ...others ] : others ;
48+ } , [ members ] ) ;
49+
3950 if ( ! isOpen ) return null ;
4051
4152 const canInvite = inviteValue . trim ( ) . length > 0 ;
4253
4354 const handleInvite = ( ) => {
44- const emails = inviteValue
45- . split ( ";" )
46- . map ( ( email ) => email . trim ( ) )
55+ const values = inviteValue
56+ . split ( / [ , ; ] / )
57+ . map ( ( v ) => v . trim ( ) )
4758 . filter ( Boolean ) ;
4859
49- if ( emails . length === 0 ) return ;
60+ if ( values . length === 0 ) return ;
5061
51- const invitedMembers = emails . map ( ( email , index ) => ( {
62+ const invitedMembers = values . map ( ( val , index ) => ( {
5263 id : `${ Date . now ( ) } -${ index } ` ,
53- name : email ,
54- avatar : email . charAt ( 0 ) . toUpperCase ( ) ,
55- role : "editor" as const
64+ name : val ,
65+ avatar : val . charAt ( 0 ) . toUpperCase ( ) ,
66+ role : "editor" as const ,
67+ isGithub : inviteMode === "github" ,
5668 } ) ) ;
5769
5870 setMembers ( ( prev ) => [ ...invitedMembers , ...prev ] ) ;
@@ -143,6 +155,30 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
143155 </ div >
144156
145157 < div className = "px-5 py-5" style = { { maxHeight : "58vh" , overflowY : "auto" } } >
158+ { /* Mode toggle */ }
159+ < div className = "flex gap-1 mb-3 p-1 rounded-xl" style = { { background : "#f3f4f6" } } >
160+ { ( [ "email" , "github" ] as const ) . map ( ( mode ) => (
161+ < button
162+ key = { mode }
163+ type = "button"
164+ onClick = { ( ) => { setInviteMode ( mode ) ; setInviteValue ( "" ) ; } }
165+ className = "flex flex-1 items-center justify-center gap-1.5 rounded-lg py-2 tracking-tight transition-all"
166+ style = { {
167+ background : inviteMode === mode ? "#ffffff" : "transparent" ,
168+ color : inviteMode === mode ? "#111827" : "#6b7280" ,
169+ fontSize : "13px" ,
170+ fontWeight : 900 ,
171+ border : "none" ,
172+ cursor : "pointer" ,
173+ boxShadow : inviteMode === mode ? "0 1px 4px rgba(0,0,0,0.10)" : "none" ,
174+ } }
175+ >
176+ { mode === "email" ? < Mail size = { 14 } /> : < Github size = { 14 } /> }
177+ { mode === "email" ? "이메일로 초대" : "GitHub ID로 초대" }
178+ </ button >
179+ ) ) }
180+ </ div >
181+
146182 < div className = "flex gap-2" >
147183 < input
148184 value = { inviteValue }
@@ -153,7 +189,11 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
153189 handleInvite ( ) ;
154190 }
155191 } }
156- placeholder = "쉼표 또는 세미콜론으로 구분된 이메일을 추가하여 초대하세요"
192+ placeholder = {
193+ inviteMode === "email"
194+ ? "쉼표 또는 세미콜론으로 구분된 이메일을 추가하여 초대하세요"
195+ : "쉼표 또는 세미콜론으로 구분된 GitHub ID를 추가하여 초대하세요"
196+ }
157197 className = "min-w-0 flex-1 rounded-lg px-4 py-3 outline-none"
158198 style = { {
159199 border : "2px solid #6d5dfc" ,
@@ -170,7 +210,7 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
170210 className = "rounded-lg border-0 px-5 py-3 tracking-tight"
171211 style = { {
172212 background : canInvite ? "#6d5dfc" : "#d1d5db" ,
173- color : canInvite ? "#ffffff" : "#ffffff" ,
213+ color : "#ffffff" ,
174214 cursor : canInvite ? "pointer" : "not-allowed" ,
175215 fontSize : "14px" ,
176216 fontWeight : 900
@@ -197,66 +237,88 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
197237 action = { < ChevronRight size = { 16 } /> }
198238 />
199239
200- { members . map ( ( member ) => (
201- < div key = { member . id } className = "flex items-center gap-3 rounded-xl px-1 py-2" >
202- < div className = "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full" style = { {
203- background : member . id === "junwoo" ? "#ff5b8a" : "linear-gradient(135deg, #20e3ff, #7c3aed)" ,
204- color : "#ffffff" ,
205- fontSize : "12px" ,
206- fontWeight : 950
207- } } >
208- { member . avatar }
209- </ div >
210- < span className = "min-w-0 flex-1 truncate tracking-tight" style = { {
211- color : "#374151" ,
212- fontSize : "14px" ,
213- fontWeight : 900
214- } } >
215- { member . name }
216- </ span >
217- { member . warning && (
218- < AlertCircle size = { 18 } style = { { color : "#f59e0b" } } />
219- ) }
220- { member . role === "owner" ? (
221- < span className = "tracking-tight" style = { { color : "#374151" , fontSize : "13px" , fontWeight : 900 } } >
222- { permissionLabels . owner }
223- </ span >
224- ) : (
225- < div className = "flex items-center gap-1.5" >
226- < select
227- value = { member . role }
228- onChange = { ( event ) => handleRoleChange ( member . id , event . target . value as TeamMember [ "role" ] ) }
229- className = "rounded-lg border-0 px-2 py-1 outline-none"
230- style = { {
231- background : "#ffffff" ,
232- color : "#374151" ,
233- fontSize : "13px" ,
234- fontWeight : 900 ,
235- cursor : "pointer"
236- } }
237- aria-label = { `${ member . name } 권한` }
238- >
239- < option value = "editor" > { permissionLabels . editor } </ option >
240- < option value = "viewer" > { permissionLabels . viewer } </ option >
241- </ select >
242- < button
243- type = "button"
244- onClick = { ( ) => handleRemoveClick ( member ) }
245- className = "flex h-8 w-8 items-center justify-center rounded-lg border-0"
246- style = { {
247- background : "rgba(239, 68, 68, 0.08)" ,
248- color : "#ef4444" ,
249- cursor : "pointer"
250- } }
251- aria-label = { `${ member . name } 삭제` }
252- title = { `${ member . name } 삭제` }
253- >
254- < Trash2 size = { 15 } />
255- </ button >
240+ { sortedMembers . map ( ( member ) => {
241+ const isMe = member . id === CURRENT_USER_ID ;
242+ return (
243+ < div
244+ key = { member . id }
245+ className = "flex items-center gap-3 rounded-xl px-1 py-2"
246+ style = { { } }
247+ >
248+ { /* Avatar */ }
249+ < div className = "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full" style = { {
250+ background : isMe ? "#ff5b8a" : member . isGithub ? "#24292e" : "linear-gradient(135deg, #20e3ff, #7c3aed)" ,
251+ color : "#ffffff" ,
252+ fontSize : "12px" ,
253+ fontWeight : 950
254+ } } >
255+ { member . isGithub ? < Github size = { 14 } /> : member . avatar }
256256 </ div >
257- ) }
258- </ div >
259- ) ) }
257+
258+ { /* Name */ }
259+ < span className = "min-w-0 flex-1 truncate tracking-tight" style = { {
260+ color : "#374151" ,
261+ fontSize : "14px" ,
262+ fontWeight : 900
263+ } } >
264+ { member . name }
265+ { isMe && (
266+ < span style = { { color : "#374151" , fontWeight : 700 , fontSize : "13px" , marginLeft : "6px" } } >
267+ (나)
268+ </ span >
269+ ) }
270+ </ span >
271+
272+ { member . warning && ! isMe && (
273+ < AlertCircle size = { 18 } style = { { color : "#f59e0b" } } />
274+ ) }
275+
276+ { /* Role / controls */ }
277+ { isMe ? (
278+ < span className = "tracking-tight" style = { { color : "#374151" , fontSize : "13px" , fontWeight : 900 } } >
279+ { permissionLabels [ member . role ] }
280+ </ span >
281+ ) : member . role === "owner" ? (
282+ < span className = "tracking-tight" style = { { color : "#374151" , fontSize : "13px" , fontWeight : 900 } } >
283+ { permissionLabels . owner }
284+ </ span >
285+ ) : (
286+ < div className = "flex items-center gap-1.5" >
287+ < select
288+ value = { member . role }
289+ onChange = { ( event ) => handleRoleChange ( member . id , event . target . value as TeamMember [ "role" ] ) }
290+ className = "rounded-lg border-0 px-2 py-1 outline-none"
291+ style = { {
292+ background : "#ffffff" ,
293+ color : "#374151" ,
294+ fontSize : "13px" ,
295+ fontWeight : 900 ,
296+ cursor : "pointer"
297+ } }
298+ aria-label = { `${ member . name } 권한` }
299+ >
300+ < option value = "editor" > { permissionLabels . editor } </ option >
301+ < option value = "viewer" > { permissionLabels . viewer } </ option >
302+ </ select >
303+ < button
304+ type = "button"
305+ onClick = { ( ) => handleRemoveClick ( member ) }
306+ className = "flex h-8 w-8 items-center justify-center rounded-lg border-0"
307+ style = { {
308+ background : "rgba(239, 68, 68, 0.08)" ,
309+ color : "#ef4444" ,
310+ cursor : "pointer"
311+ } }
312+ aria-label = { `${ member . name } 삭제` }
313+ title = { `${ member . name } 삭제` }
314+ >
315+ < Trash2 size = { 15 } />
316+ </ button >
317+ </ div >
318+ ) }
319+ </ div >
320+ ) ;
321+ } ) }
260322 </ div >
261323 </ div >
262324 </ div >
0 commit comments