SarahXia0405 commited on
Commit
6ab9aff
·
verified ·
1 Parent(s): cb577c7

Update web/src/components/sidebar/LeftSidebar.tsx

Browse files
web/src/components/sidebar/LeftSidebar.tsx CHANGED
@@ -3,11 +3,11 @@ import React, { useEffect, useMemo, useState } from "react";
3
  import { Separator } from "../ui/separator";
4
  import { Button } from "../ui/button";
5
  import { Input } from "../ui/input";
 
6
 
7
  import { SavedChatSection } from "./SavedChatSection";
8
- import { GroupMembers } from "../GroupMembers";
9
 
10
- import { Mail, Pencil, Check, X } from "lucide-react";
11
 
12
  import type {
13
  SavedChat,
@@ -21,19 +21,7 @@ import type {
21
  } from "../../App";
22
  import type { CourseDirectoryItem } from "../../lib/courseDirectory";
23
 
24
- /**
25
- * Behavior:
26
- * - My Space: show "Group {groupNo}" + editable "Group {Name}"
27
- * - Team/Group Space: show a WHITE card with:
28
- * - title: "Group Name" + editable group name (pencil)
29
- * - right: Invite button
30
- * - body: GroupMembers list
31
- *
32
- * Layout:
33
- * - TOP: Welcome + Course + Group block => non-scroll
34
- * - MIDDLE: Saved Chat => only scroll
35
- * - BOTTOM: Instructor/TA => fixed, non-scroll
36
- */
37
 
38
  type Props = {
39
  learningMode: LearningMode;
@@ -73,11 +61,14 @@ type Props = {
73
  selectedCourse: string;
74
  availableCourses: CourseDirectoryItem[];
75
 
 
76
  onInviteGroupMembers?: () => void;
77
 
78
- // preferred: wire to backend rename (POST /api/workspaces/{id}/rename)
79
- // fallback: localStorage
80
  onRenameGroupName?: (workspaceId: string, newName: string) => Promise<void> | void;
 
 
 
81
  };
82
 
83
  function gmailComposeLink(email: string, subject?: string, body?: string) {
@@ -110,6 +101,12 @@ function pickAny(obj: any, keys: string[]) {
110
  return undefined;
111
  }
112
 
 
 
 
 
 
 
113
  export function LeftSidebar(props: Props) {
114
  const {
115
  user,
@@ -126,6 +123,7 @@ export function LeftSidebar(props: Props) {
126
  availableCourses,
127
  onInviteGroupMembers,
128
  onRenameGroupName,
 
129
  } = props;
130
 
131
  const currentWorkspace = useMemo(
@@ -137,7 +135,6 @@ export function LeftSidebar(props: Props) {
137
  const courseInfo = useMemo((): CourseDirectoryItem | null => {
138
  const list = Array.isArray(availableCourses) ? availableCourses : [];
139
 
140
- // 1) selectedCourse: may be id or name
141
  const selRaw = (selectedCourse || "").trim();
142
  const sel = norm(selRaw);
143
  if (sel) {
@@ -147,7 +144,6 @@ export function LeftSidebar(props: Props) {
147
  if (hit) return hit;
148
  }
149
 
150
- // 2) workspace.courseInfo: may have id/name, sometimes includes instructor/TA already
151
  const wsCourse = (currentWorkspace as any)?.courseInfo as
152
  | { id?: string; name?: string; instructor?: any; teachingAssistant?: any }
153
  | undefined;
@@ -169,8 +165,8 @@ export function LeftSidebar(props: Props) {
169
  return (courseInfo as any)?.name ?? (selectedCourse || "Course");
170
  }, [courseInfo, selectedCourse]);
171
 
172
- // --------- Group number/name ---------
173
- const groupNo = useMemo(() => {
174
  const ws: any = currentWorkspace as any;
175
  return (
176
  ws?.groupNo ??
@@ -182,52 +178,67 @@ export function LeftSidebar(props: Props) {
182
  );
183
  }, [currentWorkspace]);
184
 
185
- const defaultGroupName = useMemo(() => {
186
  const ws: any = currentWorkspace as any;
187
- return pickAny(ws, ["groupName", "name", "title"]) || "My Group";
188
  }, [currentWorkspace]);
189
 
 
190
  const groupNameStorageKey = useMemo(
191
  () => `clare_group_name__${currentWorkspaceId}`,
192
  [currentWorkspaceId]
193
  );
 
 
 
 
194
 
195
- const [groupName, setGroupName] = useState<string>(defaultGroupName);
 
196
  const [editingGroupName, setEditingGroupName] = useState(false);
197
- const [draftGroupName, setDraftGroupName] = useState<string>(defaultGroupName);
 
 
 
 
 
198
 
 
199
  useEffect(() => {
200
- const stored =
 
201
  typeof window !== "undefined" ? window.localStorage.getItem(groupNameStorageKey) : null;
202
- const name = stored && stored.trim() ? stored : defaultGroupName;
203
  setGroupName(name);
204
  setDraftGroupName(name);
205
  setEditingGroupName(false);
206
- }, [groupNameStorageKey, defaultGroupName]);
 
 
 
 
 
 
 
 
207
 
208
  const saveGroupName = async () => {
209
  const next = (draftGroupName || "").trim() || "My Group";
210
-
211
- // optimistic UI
212
  setGroupName(next);
213
  setDraftGroupName(next);
214
  setEditingGroupName(false);
215
 
216
- // preferred: call provided handler (backend)
217
  if (onRenameGroupName) {
218
  try {
219
  await onRenameGroupName(currentWorkspaceId, next);
220
  return;
221
  } catch {
222
- // fallback below
223
  }
224
  }
225
-
226
  try {
227
  window.localStorage.setItem(groupNameStorageKey, next);
228
- } catch {
229
- // ignore
230
- }
231
  };
232
 
233
  const cancelGroupName = () => {
@@ -235,6 +246,30 @@ export function LeftSidebar(props: Props) {
235
  setEditingGroupName(false);
236
  };
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  // --------- Contacts (fixed bottom) ---------
239
  const instructorName = (courseInfo as any)?.instructor?.name ?? "N/A";
240
  const instructorEmail = String((courseInfo as any)?.instructor?.email ?? "").trim();
@@ -244,12 +279,13 @@ export function LeftSidebar(props: Props) {
244
 
245
  const displayName = useMemo(() => getUserName(user), [user]);
246
 
247
- // Team/Group space detection
248
  const isTeamSpace = useMemo(() => {
249
  const st = String(spaceType || "").toLowerCase();
250
  return st === "group" || st === "team";
251
  }, [spaceType]);
252
 
 
 
253
  return (
254
  <div className="h-full w-full flex flex-col min-h-0 bg-white">
255
  {/* ================= TOP (non-scroll) ================= */}
@@ -267,8 +303,8 @@ export function LeftSidebar(props: Props) {
267
  <div className="px-4 pt-10 pb-10 space-y-4">
268
  <div className="text-[30px] leading-tight font-semibold">{courseName}</div>
269
 
270
- {/* My Space: simple Group No + Group Name */}
271
  {!isTeamSpace ? (
 
272
  <div className="space-y-3">
273
  <div className="text-[30px] leading-tight font-semibold">
274
  Group {String(groupNo)}
@@ -282,7 +318,7 @@ export function LeftSidebar(props: Props) {
282
  </div>
283
  <button
284
  type="button"
285
- className="inline-flex items-center gap-1 text-muted-foreground hover:text-foreground"
286
  onClick={() => setEditingGroupName(true)}
287
  aria-label="Edit group name"
288
  >
@@ -301,24 +337,10 @@ export function LeftSidebar(props: Props) {
301
  className="h-9"
302
  autoFocus
303
  />
304
- <Button
305
- type="button"
306
- size="icon"
307
- variant="ghost"
308
- className="h-9 w-9"
309
- onClick={saveGroupName}
310
- aria-label="Save group name"
311
- >
312
  <Check className="w-4 h-4" />
313
  </Button>
314
- <Button
315
- type="button"
316
- size="icon"
317
- variant="ghost"
318
- className="h-9 w-9"
319
- onClick={cancelGroupName}
320
- aria-label="Cancel edit group name"
321
- >
322
  <X className="w-4 h-4" />
323
  </Button>
324
  </div>
@@ -326,17 +348,18 @@ export function LeftSidebar(props: Props) {
326
  </div>
327
  </div>
328
  ) : (
329
- // Team/Group Space: white card + "Group Name" header + pencil rename + Invite
330
  <div className="rounded-2xl border bg-white overflow-hidden">
331
- <div className="px-4 py-3 flex items-center justify-between">
332
- <div className="flex items-center gap-2 min-w-0">
333
- <div className="text-[14px] font-semibold text-foreground whitespace-nowrap">
 
334
  Group Name
335
  </div>
336
 
337
  {!editingGroupName ? (
338
- <div className="flex items-center gap-2 min-w-0">
339
- <div className="text-[14px] font-medium text-muted-foreground truncate max-w-[180px]">
340
  {groupName}
341
  </div>
342
  <button
@@ -347,7 +370,7 @@ export function LeftSidebar(props: Props) {
347
  >
348
  <Pencil className="w-4 h-4" />
349
  </button>
350
- </div>
351
  ) : (
352
  <div className="flex items-center gap-2 min-w-0">
353
  <Input
@@ -360,44 +383,132 @@ export function LeftSidebar(props: Props) {
360
  className="h-8 w-[180px]"
361
  autoFocus
362
  />
363
- <Button
364
- type="button"
365
- size="icon"
366
- variant="ghost"
367
- className="h-8 w-8"
368
- onClick={saveGroupName}
369
- aria-label="Save group name"
370
- >
371
  <Check className="w-4 h-4" />
372
  </Button>
373
- <Button
374
- type="button"
375
- size="icon"
376
- variant="ghost"
377
- className="h-8 w-8"
378
- onClick={cancelGroupName}
379
- aria-label="Cancel edit group name"
380
- >
381
  <X className="w-4 h-4" />
382
  </Button>
383
  </div>
384
  )}
385
  </div>
 
386
 
387
- <Button
388
- type="button"
389
- variant="secondary"
390
- className="h-9 px-3 text-[13px]"
391
- onClick={onInviteGroupMembers}
392
- >
393
- <Mail className="w-4 h-4 mr-2" />
394
- Invite
395
- </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  </div>
397
 
398
- <div className="px-3 pb-3">
399
- {/* Old GroupMembers list UI */}
400
- <GroupMembers members={groupMembers as any} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  </div>
402
  </div>
403
  )}
 
3
  import { Separator } from "../ui/separator";
4
  import { Button } from "../ui/button";
5
  import { Input } from "../ui/input";
6
+ import { Badge } from "../ui/badge";
7
 
8
  import { SavedChatSection } from "./SavedChatSection";
 
9
 
10
+ import { Pencil, Check, X, MailPlus, Users } from "lucide-react";
11
 
12
  import type {
13
  SavedChat,
 
21
  } from "../../App";
22
  import type { CourseDirectoryItem } from "../../lib/courseDirectory";
23
 
24
+ import clareAvatar from "../../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  type Props = {
27
  learningMode: LearningMode;
 
61
  selectedCourse: string;
62
  availableCourses: CourseDirectoryItem[];
63
 
64
+ // invite flow (optional)
65
  onInviteGroupMembers?: () => void;
66
 
67
+ // rename group name (optional -> fallback localStorage)
 
68
  onRenameGroupName?: (workspaceId: string, newName: string) => Promise<void> | void;
69
+
70
+ // rename group number (optional -> fallback localStorage)
71
+ onRenameGroupNo?: (workspaceId: string, newNo: number) => Promise<void> | void;
72
  };
73
 
74
  function gmailComposeLink(email: string, subject?: string, body?: string) {
 
101
  return undefined;
102
  }
103
 
104
+ function toIntOrFallback(v: any, fb: number) {
105
+ const n = Number(v);
106
+ if (Number.isFinite(n) && n > 0) return Math.floor(n);
107
+ return fb;
108
+ }
109
+
110
  export function LeftSidebar(props: Props) {
111
  const {
112
  user,
 
123
  availableCourses,
124
  onInviteGroupMembers,
125
  onRenameGroupName,
126
+ onRenameGroupNo,
127
  } = props;
128
 
129
  const currentWorkspace = useMemo(
 
135
  const courseInfo = useMemo((): CourseDirectoryItem | null => {
136
  const list = Array.isArray(availableCourses) ? availableCourses : [];
137
 
 
138
  const selRaw = (selectedCourse || "").trim();
139
  const sel = norm(selRaw);
140
  if (sel) {
 
144
  if (hit) return hit;
145
  }
146
 
 
147
  const wsCourse = (currentWorkspace as any)?.courseInfo as
148
  | { id?: string; name?: string; instructor?: any; teachingAssistant?: any }
149
  | undefined;
 
165
  return (courseInfo as any)?.name ?? (selectedCourse || "Course");
166
  }, [courseInfo, selectedCourse]);
167
 
168
+ // --------- Group number/name sources ---------
169
+ const wsGroupNo = useMemo(() => {
170
  const ws: any = currentWorkspace as any;
171
  return (
172
  ws?.groupNo ??
 
178
  );
179
  }, [currentWorkspace]);
180
 
181
+ const wsGroupName = useMemo(() => {
182
  const ws: any = currentWorkspace as any;
183
+ return pickAny(ws, ["groupName", "name", "title"]) || "";
184
  }, [currentWorkspace]);
185
 
186
+ // local storage keys
187
  const groupNameStorageKey = useMemo(
188
  () => `clare_group_name__${currentWorkspaceId}`,
189
  [currentWorkspaceId]
190
  );
191
+ const groupNoStorageKey = useMemo(
192
+ () => `clare_group_no__${currentWorkspaceId}`,
193
+ [currentWorkspaceId]
194
+ );
195
 
196
+ // state: group name (editable)
197
+ const [groupName, setGroupName] = useState<string>(wsGroupName || "My Group");
198
  const [editingGroupName, setEditingGroupName] = useState(false);
199
+ const [draftGroupName, setDraftGroupName] = useState<string>(wsGroupName || "My Group");
200
+
201
+ // state: group no (editable)
202
+ const [groupNo, setGroupNo] = useState<number>(toIntOrFallback(wsGroupNo, 1));
203
+ const [editingGroupNo, setEditingGroupNo] = useState(false);
204
+ const [draftGroupNo, setDraftGroupNo] = useState<string>(String(toIntOrFallback(wsGroupNo, 1)));
205
 
206
+ // hydrate from localStorage (prefer persisted values)
207
  useEffect(() => {
208
+ // group name
209
+ const storedName =
210
  typeof window !== "undefined" ? window.localStorage.getItem(groupNameStorageKey) : null;
211
+ const name = storedName && storedName.trim() ? storedName : (wsGroupName || "My Group");
212
  setGroupName(name);
213
  setDraftGroupName(name);
214
  setEditingGroupName(false);
215
+
216
+ // group no
217
+ const storedNo =
218
+ typeof window !== "undefined" ? window.localStorage.getItem(groupNoStorageKey) : null;
219
+ const no = storedNo && storedNo.trim() ? toIntOrFallback(storedNo, toIntOrFallback(wsGroupNo, 1)) : toIntOrFallback(wsGroupNo, 1);
220
+ setGroupNo(no);
221
+ setDraftGroupNo(String(no));
222
+ setEditingGroupNo(false);
223
+ }, [groupNameStorageKey, groupNoStorageKey, wsGroupName, wsGroupNo]);
224
 
225
  const saveGroupName = async () => {
226
  const next = (draftGroupName || "").trim() || "My Group";
 
 
227
  setGroupName(next);
228
  setDraftGroupName(next);
229
  setEditingGroupName(false);
230
 
 
231
  if (onRenameGroupName) {
232
  try {
233
  await onRenameGroupName(currentWorkspaceId, next);
234
  return;
235
  } catch {
236
+ // fallback
237
  }
238
  }
 
239
  try {
240
  window.localStorage.setItem(groupNameStorageKey, next);
241
+ } catch {}
 
 
242
  };
243
 
244
  const cancelGroupName = () => {
 
246
  setEditingGroupName(false);
247
  };
248
 
249
+ const saveGroupNo = async () => {
250
+ const nextNo = toIntOrFallback(draftGroupNo, groupNo);
251
+ setGroupNo(nextNo);
252
+ setDraftGroupNo(String(nextNo));
253
+ setEditingGroupNo(false);
254
+
255
+ if (onRenameGroupNo) {
256
+ try {
257
+ await onRenameGroupNo(currentWorkspaceId, nextNo);
258
+ return;
259
+ } catch {
260
+ // fallback
261
+ }
262
+ }
263
+ try {
264
+ window.localStorage.setItem(groupNoStorageKey, String(nextNo));
265
+ } catch {}
266
+ };
267
+
268
+ const cancelGroupNo = () => {
269
+ setDraftGroupNo(String(groupNo));
270
+ setEditingGroupNo(false);
271
+ };
272
+
273
  // --------- Contacts (fixed bottom) ---------
274
  const instructorName = (courseInfo as any)?.instructor?.name ?? "N/A";
275
  const instructorEmail = String((courseInfo as any)?.instructor?.email ?? "").trim();
 
279
 
280
  const displayName = useMemo(() => getUserName(user), [user]);
281
 
 
282
  const isTeamSpace = useMemo(() => {
283
  const st = String(spaceType || "").toLowerCase();
284
  return st === "group" || st === "team";
285
  }, [spaceType]);
286
 
287
+ const memberCount = (groupMembers || []).length;
288
+
289
  return (
290
  <div className="h-full w-full flex flex-col min-h-0 bg-white">
291
  {/* ================= TOP (non-scroll) ================= */}
 
303
  <div className="px-4 pt-10 pb-10 space-y-4">
304
  <div className="text-[30px] leading-tight font-semibold">{courseName}</div>
305
 
 
306
  {!isTeamSpace ? (
307
+ // My Space: keep simple
308
  <div className="space-y-3">
309
  <div className="text-[30px] leading-tight font-semibold">
310
  Group {String(groupNo)}
 
318
  </div>
319
  <button
320
  type="button"
321
+ className="inline-flex items-center text-muted-foreground hover:text-foreground"
322
  onClick={() => setEditingGroupName(true)}
323
  aria-label="Edit group name"
324
  >
 
337
  className="h-9"
338
  autoFocus
339
  />
340
+ <Button type="button" size="icon" variant="ghost" className="h-9 w-9" onClick={saveGroupName}>
 
 
 
 
 
 
 
341
  <Check className="w-4 h-4" />
342
  </Button>
343
+ <Button type="button" size="icon" variant="ghost" className="h-9 w-9" onClick={cancelGroupName}>
 
 
 
 
 
 
 
344
  <X className="w-4 h-4" />
345
  </Button>
346
  </div>
 
348
  </div>
349
  </div>
350
  ) : (
351
+ // Team/Group Space: exact structure per screenshot
352
  <div className="rounded-2xl border bg-white overflow-hidden">
353
+ {/* Row 1: Group Name + (value) + pencil (NO invite here) */}
354
+ <div className="px-4 pt-4 pb-2">
355
+ <div className="flex items-center gap-3 min-w-0">
356
+ <div className="text-[18px] font-semibold text-foreground whitespace-nowrap">
357
  Group Name
358
  </div>
359
 
360
  {!editingGroupName ? (
361
+ <>
362
+ <div className="text-[18px] font-medium text-muted-foreground truncate max-w-[180px]">
363
  {groupName}
364
  </div>
365
  <button
 
370
  >
371
  <Pencil className="w-4 h-4" />
372
  </button>
373
+ </>
374
  ) : (
375
  <div className="flex items-center gap-2 min-w-0">
376
  <Input
 
383
  className="h-8 w-[180px]"
384
  autoFocus
385
  />
386
+ <Button type="button" size="icon" variant="ghost" className="h-8 w-8" onClick={saveGroupName}>
 
 
 
 
 
 
 
387
  <Check className="w-4 h-4" />
388
  </Button>
389
+ <Button type="button" size="icon" variant="ghost" className="h-8 w-8" onClick={cancelGroupName}>
 
 
 
 
 
 
 
390
  <X className="w-4 h-4" />
391
  </Button>
392
  </div>
393
  )}
394
  </div>
395
+ </div>
396
 
397
+ {/* Row 2: Group # (2) + pencil(edit group no) | Invite button on right */}
398
+ <div className="px-4 pb-3">
399
+ <div className="flex items-center justify-between gap-3">
400
+ <div className="flex items-center gap-2 min-w-0">
401
+ <Users className="w-4 h-4 text-muted-foreground" />
402
+
403
+ {!editingGroupNo ? (
404
+ <>
405
+ <div className="text-[16px] text-foreground font-medium whitespace-nowrap">
406
+ Group # ({memberCount})
407
+ </div>
408
+ <div className="text-[16px] text-muted-foreground font-medium whitespace-nowrap">
409
+ {String(groupNo)}
410
+ </div>
411
+ <button
412
+ type="button"
413
+ className="inline-flex items-center text-muted-foreground hover:text-foreground"
414
+ onClick={() => setEditingGroupNo(true)}
415
+ aria-label="Edit group number"
416
+ >
417
+ <Pencil className="w-4 h-4" />
418
+ </button>
419
+ </>
420
+ ) : (
421
+ <div className="flex items-center gap-2">
422
+ <div className="text-[16px] text-foreground font-medium whitespace-nowrap">
423
+ Group # ({memberCount})
424
+ </div>
425
+ <Input
426
+ value={draftGroupNo}
427
+ onChange={(e) => setDraftGroupNo(e.target.value)}
428
+ onKeyDown={(e) => {
429
+ if (e.key === "Enter") saveGroupNo();
430
+ if (e.key === "Escape") cancelGroupNo();
431
+ }}
432
+ className="h-8 w-[90px]"
433
+ autoFocus
434
+ />
435
+ <Button type="button" size="icon" variant="ghost" className="h-8 w-8" onClick={saveGroupNo}>
436
+ <Check className="w-4 h-4" />
437
+ </Button>
438
+ <Button type="button" size="icon" variant="ghost" className="h-8 w-8" onClick={cancelGroupNo}>
439
+ <X className="w-4 h-4" />
440
+ </Button>
441
+ </div>
442
+ )}
443
+ </div>
444
+
445
+ <Button
446
+ type="button"
447
+ variant="secondary"
448
+ className="h-9 px-3 text-[13px]"
449
+ onClick={onInviteGroupMembers}
450
+ >
451
+ <MailPlus className="w-4 h-4 mr-2" />
452
+ Invite
453
+ </Button>
454
+ </div>
455
  </div>
456
 
457
+ {/* Member list */}
458
+ <div className="px-3 pb-3 space-y-2">
459
+ {(groupMembers || []).map((member) => {
460
+ const isAI = !!(member as any).isAI;
461
+ const name = String((member as any).name || "");
462
+ const email = String((member as any).email || "");
463
+ const initials = name
464
+ ? name
465
+ .split(" ")
466
+ .filter(Boolean)
467
+ .map((n) => n[0])
468
+ .join("")
469
+ .toUpperCase()
470
+ : "?";
471
+
472
+ return (
473
+ <div
474
+ key={(member as any).id}
475
+ className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors"
476
+ >
477
+ {/* Avatar */}
478
+ <div
479
+ className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
480
+ isAI ? "overflow-hidden bg-white" : "bg-muted"
481
+ }`}
482
+ >
483
+ {isAI ? (
484
+ <img
485
+ src={clareAvatar}
486
+ alt="Clare"
487
+ className="w-full h-full object-cover"
488
+ />
489
+ ) : (
490
+ <span className="text-sm">{initials}</span>
491
+ )}
492
+ </div>
493
+
494
+ {/* Member Info */}
495
+ <div className="flex-1 min-w-0">
496
+ <div className="flex items-center gap-2">
497
+ <p className="text-sm truncate text-foreground">{name}</p>
498
+ {isAI && (
499
+ <Badge variant="secondary" className="text-xs">
500
+ AI
501
+ </Badge>
502
+ )}
503
+ </div>
504
+ <p className="text-xs text-muted-foreground truncate">{email}</p>
505
+ </div>
506
+
507
+ {/* Online Status */}
508
+ <div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" title="Online" />
509
+ </div>
510
+ );
511
+ })}
512
  </div>
513
  </div>
514
  )}