Skip to content

Commit d8dbdd6

Browse files
committed
improve invite chatroom dialog
1 parent 503f572 commit d8dbdd6

File tree

9 files changed

+441
-198
lines changed

9 files changed

+441
-198
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
"use client";
2+
3+
import { Check, Plus } from "lucide-react";
4+
5+
import { Button } from "@shared/components/ui/button";
6+
import {
7+
Command,
8+
CommandEmpty,
9+
CommandGroup,
10+
CommandInput,
11+
CommandItem,
12+
CommandList,
13+
} from "@shared/components/ui/command";
14+
import {
15+
Dialog,
16+
DialogContent,
17+
DialogDescription,
18+
DialogFooter,
19+
DialogHeader,
20+
DialogTitle,
21+
} from "@shared/components/ui/dialog";
22+
import { useEffect, useState } from "react";
23+
import {
24+
Avatar,
25+
AvatarFallback,
26+
AvatarImage,
27+
} from "@/shared/components/ui/avatar";
28+
import { createClient } from "@/shared/utils/supabase/client";
29+
import { toast } from "sonner";
30+
import { inviteUserToChatroom } from "../../actions";
31+
32+
interface ChatroomMembers {
33+
chatroom_id: string;
34+
created_at: string;
35+
id: number;
36+
is_active: boolean;
37+
member_id: number;
38+
Classroom_Members: {
39+
id: number;
40+
user_id: string;
41+
classroom_id: number;
42+
Users: {
43+
id: string;
44+
full_name: string | null;
45+
avatar_url: string | null;
46+
};
47+
};
48+
}
49+
50+
interface ClassroomMember {
51+
id: number;
52+
user_id: string;
53+
classroom_id: number;
54+
Users: {
55+
email: string | null;
56+
full_name: string | null;
57+
avatar_url: string | null;
58+
};
59+
}
60+
61+
export function InviteChatroomButton({
62+
chatroomId,
63+
classroomId,
64+
chatroomMembers,
65+
}: {
66+
chatroomId: string;
67+
classroomId: number;
68+
chatroomMembers: ChatroomMembers[];
69+
}) {
70+
const [open, setOpen] = useState(false);
71+
const [selectedUsers, setSelectedUsers] = useState<ClassroomMember[]>([]);
72+
const [classroomInvitees, setClassroomInvitees] = useState<ClassroomMember[]>(
73+
[]
74+
);
75+
76+
// get a list of Classroom_Members id
77+
const currentMemberIds = chatroomMembers.map(
78+
(member) => member.Classroom_Members.id
79+
);
80+
81+
useEffect(() => {
82+
async function fetchClassroomMembers() {
83+
const supabase = createClient();
84+
85+
const { data, error } = await supabase
86+
.from("Classroom_Members")
87+
.select(
88+
`
89+
id,
90+
user_id,
91+
classroom_id,
92+
Users (
93+
email,
94+
full_name,
95+
avatar_url
96+
)
97+
`
98+
)
99+
.eq("classroom_id", classroomId);
100+
101+
if (error) {
102+
console.error("Error fetching classroom members:", error);
103+
} else {
104+
setClassroomInvitees(data as ClassroomMember[]);
105+
}
106+
}
107+
108+
if (open) {
109+
fetchClassroomMembers();
110+
}
111+
}, [classroomId, currentMemberIds, open]);
112+
113+
const handleInviteUsers = async () => {
114+
if (selectedUsers.length === 0) return;
115+
116+
try {
117+
const invitePromises = selectedUsers.map((invitee) =>
118+
inviteUserToChatroom(chatroomId, invitee.Users.email || "")
119+
);
120+
121+
await Promise.all(invitePromises);
122+
123+
setOpen(false);
124+
setSelectedUsers([]);
125+
toast.success("Successfully invited user(s) to chatroom");
126+
} catch (error) {
127+
console.error("Error inviting users:", error);
128+
toast.error("Error inviting user(s)");
129+
}
130+
};
131+
132+
return (
133+
<>
134+
<Button size="sm" variant="outline" onClick={() => setOpen(true)}>
135+
<Plus />
136+
Invite
137+
</Button>
138+
<Dialog
139+
open={open}
140+
onOpenChange={(isOpen) => {
141+
setOpen(isOpen);
142+
if (!isOpen) {
143+
setSelectedUsers([]);
144+
}
145+
}}
146+
>
147+
<DialogContent className="gap-0 p-0 outline-none">
148+
<DialogHeader className="px-4 pb-4 pt-5">
149+
<DialogTitle>New chatroom Member</DialogTitle>
150+
<DialogDescription>
151+
Invite a user to this chatroom. You can only invite members of
152+
current classroom.
153+
</DialogDescription>
154+
</DialogHeader>
155+
<Command className="overflow-hidden rounded-t-none border-t bg-transparent">
156+
<CommandInput placeholder="Search member..." />
157+
<CommandList>
158+
<CommandEmpty>No members found.</CommandEmpty>
159+
<CommandGroup className="p-2">
160+
{classroomInvitees.map((user) => (
161+
<CommandItem
162+
key={user.id.toString()}
163+
className="flex items-center px-2"
164+
onSelect={() => {
165+
if (
166+
selectedUsers.some(
167+
(selected) => selected.id === user.id
168+
)
169+
) {
170+
setSelectedUsers(
171+
selectedUsers.filter(
172+
(selectedUser) => selectedUser.id !== user.id
173+
)
174+
);
175+
} else {
176+
setSelectedUsers([...selectedUsers, user]);
177+
}
178+
}}
179+
>
180+
<Avatar>
181+
<AvatarImage
182+
src={user.Users.avatar_url || ""}
183+
alt="Image"
184+
/>
185+
<AvatarFallback>
186+
{user.Users.full_name?.[0] || "U"}
187+
</AvatarFallback>
188+
</Avatar>
189+
<div className="ml-2">
190+
<p className="text-sm font-medium leading-none">
191+
{user.Users.full_name || "Unknown"}
192+
</p>
193+
<p className="text-sm text-muted-foreground">
194+
{user.Users.email || "No email"}
195+
</p>
196+
</div>
197+
{selectedUsers.some(
198+
(selected) => selected.id === user.id
199+
) ? (
200+
<Check className="ml-auto flex h-5 w-5 text-primary" />
201+
) : null}
202+
</CommandItem>
203+
))}
204+
</CommandGroup>
205+
</CommandList>
206+
</Command>
207+
<DialogFooter className="flex items-center border-t p-4 sm:justify-between">
208+
{selectedUsers.length > 0 ? (
209+
<div className="flex -space-x-2 overflow-hidden">
210+
{selectedUsers.map((user) => (
211+
<Avatar
212+
key={user.Users.email}
213+
className="inline-block border-2 border-background"
214+
>
215+
<AvatarImage src={user.Users.avatar_url!} />
216+
<AvatarFallback>{user.Users.full_name![0]}</AvatarFallback>
217+
</Avatar>
218+
))}
219+
</div>
220+
) : (
221+
<p className="text-sm text-muted-foreground">
222+
Select users to add to this thread.
223+
</p>
224+
)}
225+
<Button
226+
disabled={selectedUsers.length <= 0}
227+
onClick={handleInviteUsers}
228+
>
229+
Continue
230+
</Button>
231+
</DialogFooter>
232+
</DialogContent>
233+
</Dialog>
234+
</>
235+
);
236+
}
237+
238+
export default InviteChatroomButton;

app/chatrooms/[chatroomId]/components/leave-chatroom-button.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
import { useState } from "react";
44
import { useRouter } from "next/navigation";
55
import { leaveChatroom } from "@/app/chatrooms/actions";
6+
import { Button } from "@/shared/components/ui/button";
7+
import { DoorOpen } from "lucide-react";
68

79
export default function LeaveChatroomButton({
10+
classroomId,
811
chatroomId,
912
}: {
13+
classroomId: number;
1014
chatroomId: string;
1115
}) {
1216
const [isLeaving, setIsLeaving] = useState(false);
@@ -17,7 +21,7 @@ export default function LeaveChatroomButton({
1721
setIsLeaving(true);
1822
try {
1923
await leaveChatroom(chatroomId);
20-
router.push("/chatrooms");
24+
router.push(`/classrooms/${classroomId}/chatrooms`);
2125
} catch (error) {
2226
console.error("Error leaving chatroom:", error);
2327
alert("Failed to leave chatroom. Please try again.");
@@ -28,12 +32,13 @@ export default function LeaveChatroomButton({
2832
};
2933

3034
return (
31-
<button
35+
<Button
3236
onClick={handleLeave}
3337
disabled={isLeaving}
34-
className="rounded bg-red-600 px-4 py-2 text-white transition-colors hover:bg-red-700 disabled:bg-gray-400"
38+
variant={"destructiveGhost"}
3539
>
36-
{isLeaving ? "Leaving..." : "Leave Chatroom"}
37-
</button>
40+
<DoorOpen />
41+
{isLeaving ? "Leaving..." : "Leave"}
42+
</Button>
3843
);
3944
}

app/chatrooms/[chatroomId]/invite/page.tsx

Lines changed: 0 additions & 73 deletions
This file was deleted.

0 commit comments

Comments
 (0)