1
1
"use client" ;
2
2
import type { Team } from "@/api/team" ;
3
3
import type { TeamAccountRole } from "@/api/team-members" ;
4
+ import { Img } from "@/components/blocks/Img" ;
4
5
import { Spinner } from "@/components/ui/Spinner/Spinner" ;
5
6
import { Button } from "@/components/ui/button" ;
6
7
import {
@@ -19,13 +20,22 @@ import {
19
20
SelectTrigger ,
20
21
SelectValue ,
21
22
} from "@/components/ui/select" ;
23
+ import { } from "@/components/ui/table" ;
24
+ import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler" ;
22
25
import { cn } from "@/lib/utils" ;
23
26
import { zodResolver } from "@hookform/resolvers/zod" ;
24
27
import { useMutation } from "@tanstack/react-query" ;
25
- import { ExternalLinkIcon , PlusIcon , Trash2Icon , UserPlus } from "lucide-react" ;
28
+ import {
29
+ CheckIcon ,
30
+ ExternalLinkIcon ,
31
+ PlusIcon ,
32
+ Trash2Icon ,
33
+ UserPlus ,
34
+ } from "lucide-react" ;
26
35
import Link from "next/link" ;
27
36
import { useForm } from "react-hook-form" ;
28
37
import { toast } from "sonner" ;
38
+ import type { ThirdwebClient } from "thirdweb" ;
29
39
import { z } from "zod" ;
30
40
import { getValidTeamPlan } from "../../../../../components/TeamHeader/getValidTeamPlan" ;
31
41
@@ -40,6 +50,12 @@ const inviteFormSchema = z.object({
40
50
. min ( 1 , "No invites added" ) ,
41
51
} ) ;
42
52
53
+ export type RecommendedMember = {
54
+ email : string ;
55
+ name : string | null ;
56
+ image : string | null ;
57
+ } ;
58
+
43
59
type InviteFormValues = z . infer < typeof inviteFormSchema > ;
44
60
45
61
// Note: This component is also used in team onboarding flow and not just in team settings page
@@ -61,6 +77,8 @@ export function InviteSection(props: {
61
77
className ?: string ;
62
78
onInviteSuccess ?: ( ) => void ;
63
79
shouldHideInviteButton ?: boolean ;
80
+ recommendedMembers : RecommendedMember [ ] ;
81
+ client : ThirdwebClient ;
64
82
} ) {
65
83
const teamPlan = getValidTeamPlan ( props . team ) ;
66
84
let bottomSection : React . ReactNode = null ;
@@ -241,6 +259,41 @@ export function InviteSection(props: {
241
259
</ p >
242
260
</ div >
243
261
262
+ { props . recommendedMembers . length > 0 && (
263
+ < RecommendedMembersSection
264
+ client = { props . client }
265
+ isDisabled = { ! inviteEnabled }
266
+ recommendedMembers = { props . recommendedMembers }
267
+ selectedMembers = { form
268
+ . watch ( "invites" )
269
+ . map ( ( invite ) => invite . email ) }
270
+ onToggleMember = { ( email ) => {
271
+ const currentInvites = form
272
+ . getValues ( "invites" )
273
+ . filter ( ( x ) => x . email !== "" ) ;
274
+
275
+ const inviteIndex = currentInvites . findIndex (
276
+ ( invite ) => invite . email === email ,
277
+ ) ;
278
+ if ( inviteIndex !== - 1 ) {
279
+ currentInvites . splice ( inviteIndex , 1 ) ;
280
+ } else {
281
+ currentInvites . push ( { email, role : "MEMBER" } ) ;
282
+ }
283
+
284
+ // must show at least one (even if its empty)
285
+ if ( currentInvites . length === 0 ) {
286
+ currentInvites . push ( { email : "" , role : "MEMBER" } ) ;
287
+ }
288
+
289
+ form . setValue ( "invites" , currentInvites , {
290
+ shouldDirty : true ,
291
+ shouldTouch : true ,
292
+ } ) ;
293
+ } }
294
+ />
295
+ ) }
296
+
244
297
< div className = "px-4 py-6 lg:px-6" >
245
298
< div className = "flex flex-col gap-5" >
246
299
{ form . watch ( "invites" ) . map ( ( _ , index ) => (
@@ -372,6 +425,7 @@ export function InviteSection(props: {
372
425
</ Button >
373
426
</ div >
374
427
</ div >
428
+
375
429
{ bottomSection }
376
430
</ div >
377
431
</ section >
@@ -410,3 +464,71 @@ function RoleSelector(props: {
410
464
</ Select >
411
465
) ;
412
466
}
467
+
468
+ function RecommendedMembersSection ( props : {
469
+ recommendedMembers : RecommendedMember [ ] ;
470
+ selectedMembers : string [ ] ;
471
+ onToggleMember : ( email : string ) => void ;
472
+ isDisabled : boolean ;
473
+ client : ThirdwebClient ;
474
+ } ) {
475
+ return (
476
+ < div className = "relative border-b" >
477
+ < div className = "px-4 py-4 lg:px-6" >
478
+ < h3 className = "font-medium text-base" > Recommended Members</ h3 >
479
+ < p className = "text-muted-foreground text-sm" >
480
+ User's that have your team's verified domain in their email address
481
+ but haven't been added to your team yet.
482
+ </ p >
483
+ </ div >
484
+ < div className = "grid max-h-[294px] grid-cols-1 gap-3 overflow-y-auto px-4 pb-6 md:grid-cols-2 lg:px-6 xl:grid-cols-3" >
485
+ { props . recommendedMembers . map ( ( member ) => {
486
+ const isSelected = props . selectedMembers . includes ( member . email ) ;
487
+ return (
488
+ < Button
489
+ key = { member . email }
490
+ variant = "outline"
491
+ className = { cn (
492
+ "relative flex h-auto w-auto items-center justify-between gap-2 rounded-lg border-dashed p-2.5 text-start hover:border-active-border hover:border-solid hover:bg-accent/50 disabled:opacity-100" ,
493
+ isSelected && "border-active-border border-solid bg-accent/50" ,
494
+ ) }
495
+ onClick = { ( ) => {
496
+ props . onToggleMember ( member . email ) ;
497
+ } }
498
+ disabled = { props . isDisabled }
499
+ >
500
+ < div className = "flex items-center gap-2.5 overflow-hidden" >
501
+ < Img
502
+ src = {
503
+ member . image
504
+ ? resolveSchemeWithErrorHandler ( {
505
+ uri : member . image ,
506
+ client : props . client ,
507
+ } )
508
+ : ""
509
+ }
510
+ className = "size-7 rounded-full border"
511
+ fallback = {
512
+ < div className = "flex items-center justify-center bg-muted/70 font-semibold text-muted-foreground uppercase" >
513
+ { member . email [ 0 ] }
514
+ </ div >
515
+ }
516
+ />
517
+
518
+ < div className = "truncate text-foreground text-sm" >
519
+ { member . email }
520
+ </ div >
521
+ </ div >
522
+
523
+ { isSelected && (
524
+ < div className = "p-1" >
525
+ < CheckIcon className = "size-5 text-foreground" />
526
+ </ div >
527
+ ) }
528
+ </ Button >
529
+ ) ;
530
+ } ) }
531
+ </ div >
532
+ </ div >
533
+ ) ;
534
+ }
0 commit comments