Skip to content

Commit c58f6f9

Browse files
waiscodeslucemans
andauthored
Update Invites & Team Membership (#206)
Co-authored-by: Luc <luc@lucemans.nl>
1 parent b83b724 commit c58f6f9

File tree

4 files changed

+121
-52
lines changed

4 files changed

+121
-52
lines changed

web/src/components/select/UserSelect.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { FC } from 'react';
22

33
import { useUsers } from '@/api';
4+
import { useTeamMembers } from '@/api/team';
45

56
import {
67
FieldSelect,
@@ -12,17 +13,25 @@ export const UserSelect: FC<
1213
value: string;
1314
name?: string;
1415
forceCategory?: string;
16+
teamId: string;
1517
onChange: (_value: string) => void;
1618
} & Partial<FieldSelectProperties>
17-
> = ({ value, name, forceCategory, onChange, ...properties }) => {
19+
> = ({ value, name, forceCategory, teamId, onChange, ...properties }) => {
1820
const { data: users } = useUsers();
21+
const { data: teamMembers } = useTeamMembers(teamId);
1922

20-
const options = (users || []).map((user) => {
21-
return {
23+
const options = (users || [])
24+
.filter((user) => {
25+
if (!teamMembers) return true;
26+
27+
return !teamMembers.some(
28+
(member) => member.user_id === user.user_id
29+
);
30+
})
31+
.map((user) => ({
2232
label: user.name,
2333
value: user.user_id,
24-
};
25-
});
34+
}));
2635

2736
return (
2837
<FieldSelect

web/src/gui/invite/InviteList.tsx

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { Link } from '@tanstack/react-router';
21
import { FC } from 'react';
2+
import { FiCopy } from 'react-icons/fi';
3+
import { toast } from 'sonner';
34

45
import { Invite } from '@/api/invite';
56
import { useTeamInvites } from '@/api/team/invite';
7+
import { useUser } from '@/api/user';
68

79
import { UserPreview } from '../user/UserPreview';
810

@@ -19,44 +21,69 @@ export const InviteList: FC<{ team_id: string }> = ({ team_id }) => {
1921
<h2 className="h2">Invites</h2>
2022
</div>
2123
{invites && (
22-
<ul className="card divide-y">
23-
{invites.map((invite) => (
24-
<InviteListItem
25-
key={invite.invite_id}
26-
invite={invite}
27-
/>
28-
))}
29-
</ul>
24+
<div className="card no-padding">
25+
<ul className="divide-y">
26+
{invites.map((invite) => (
27+
<InviteListItem
28+
key={invite.invite_id}
29+
invite={invite}
30+
/>
31+
))}
32+
</ul>
33+
</div>
3034
)}
3135
</div>
3236
);
3337
};
3438

3539
const InviteListItem: FC<{ invite: Invite }> = ({ invite }) => {
40+
const { data: user } = useUser(invite.sender_id);
41+
42+
const handleCopy = async () => {
43+
const inviteUrl = `${window.location.origin}/invite/${invite.invite_id}`;
44+
45+
await navigator.clipboard.writeText(inviteUrl);
46+
toast.success('Invite link copied to clipboard');
47+
};
48+
3649
return (
37-
<li className="flex items-center justify-between py-2 first:pt-0 last:pb-0">
50+
<li className="grid grid-cols-[1fr,1fr,1fr,32px] items-center gap-4 py-3 pl-4 pr-6">
3851
<div>
3952
{invite.user_id ? (
40-
<div className="flex items-center gap-2">
41-
<UserPreview user_id={invite.user_id} />
42-
<div>invited by</div>
43-
<div>
44-
<UserPreview user_id={invite.sender_id} />
45-
</div>
46-
</div>
53+
<UserPreview variant="inline" user_id={invite.user_id} />
4754
) : (
48-
<>Anonymous Invite Link</>
55+
<span className="text-muted-foreground">
56+
Anonymous Invite Link
57+
</span>
4958
)}
5059
</div>
60+
{invite.sender_id !== user?.user_id ? (
61+
<div className="flex flex-col text-center">
62+
<div className="text-sm">Invited By</div>
63+
<div>
64+
<UserPreview
65+
variant="inline"
66+
user_id={invite.sender_id}
67+
/>
68+
</div>
69+
</div>
70+
) : (
71+
<div></div>
72+
)}
73+
<div className="text-center">
74+
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold capitalize">
75+
{invite.status}
76+
</span>
77+
</div>
5178
{invite.status === 'pending' && (
52-
<>
53-
<Link
54-
to={'/invite/$inviteId'}
55-
params={{ inviteId: invite.invite_id }}
79+
<div className="flex justify-end">
80+
<button
81+
onClick={handleCopy}
82+
className="text-primary hover:text-primary/80"
5683
>
57-
{invite.invite_id}
58-
</Link>
59-
</>
84+
<FiCopy className="size-4" />
85+
</button>
86+
</div>
6087
)}
6188
</li>
6289
);

web/src/gui/invite/TeamInviteCreateModal.tsx

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { FC, PropsWithChildren, useState } from 'react';
22
import { FiLink, FiUserPlus } from 'react-icons/fi';
3+
import { toast } from 'sonner';
34

45
import { useTeamInviteCreate } from '@/api/team';
56
import { Button } from '@/components/button';
67
import {
8+
ModalClose,
79
ModalContent,
810
ModalDescription,
911
ModalRoot,
@@ -21,6 +23,34 @@ export const TeamInviteCreateModal: FC<
2123
});
2224
const isDisabled = !userId;
2325

26+
const handleCreateInvite = (userId?: string) => {
27+
createInvite(
28+
{ userId },
29+
{
30+
onSuccess: (data) => {
31+
if (!userId) {
32+
const inviteUrl = `${window.location.origin}/invite/${data.invite_id}`;
33+
34+
navigator.clipboard.writeText(inviteUrl);
35+
}
36+
37+
toast.success(
38+
userId
39+
? 'Invite sent successfully'
40+
: 'Invite link copied to clipboard'
41+
);
42+
setUserId('');
43+
},
44+
onError: () =>
45+
toast.error(
46+
userId
47+
? 'Failed to send invite'
48+
: 'Failed to create invite link'
49+
),
50+
}
51+
);
52+
};
53+
2454
return (
2555
<>
2656
<ModalRoot>
@@ -35,6 +65,7 @@ export const TeamInviteCreateModal: FC<
3565
<UserSelect
3666
placeholder="Choose an individual"
3767
value={userId}
68+
teamId={team_id}
3869
onChange={(user) => {
3970
setUserId(user);
4071

@@ -43,25 +74,27 @@ export const TeamInviteCreateModal: FC<
4374
/>
4475

4576
<div className="flex flex-row gap-2">
46-
<Button
47-
className="w-full"
48-
onClick={() => createInvite({})}
49-
>
50-
<FiLink /> Copy link
51-
</Button>
52-
<Button
53-
className="w-full"
54-
variant="primary"
55-
disabled={isDisabled}
56-
onClick={() => {
57-
if (isDisabled) return;
58-
59-
createInvite({ userId });
60-
}}
61-
>
62-
<FiUserPlus />
63-
Send
64-
</Button>
77+
<ModalClose asChild>
78+
<Button
79+
className="w-full"
80+
onClick={() => handleCreateInvite()}
81+
>
82+
<FiLink /> Copy link
83+
</Button>
84+
</ModalClose>
85+
<ModalClose asChild>
86+
<Button
87+
className="w-full"
88+
variant="primary"
89+
disabled={isDisabled}
90+
onClick={() => {
91+
handleCreateInvite(userId);
92+
}}
93+
>
94+
<FiUserPlus />
95+
Send
96+
</Button>
97+
</ModalClose>
6598
</div>
6699
</ModalContent>
67100
</ModalRoot>

web/src/gui/user/MemberList.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ export const MemberList: FC<{ team_id: string }> = ({ team_id }) => {
88
const { data: members } = useTeamMembers(team_id);
99

1010
return (
11-
<div className="space-y-2">
11+
<>
1212
{members && (
13-
<ul className="divide-y">
13+
<ul className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
1414
{members.map((member) => (
1515
<li key={member.user_id}>
1616
<UserPreview user={member} />
1717
</li>
1818
))}
1919
</ul>
2020
)}
21-
</div>
21+
</>
2222
);
2323
};

0 commit comments

Comments
 (0)