SarahXia0405 commited on
Commit
d1e9766
·
verified ·
1 Parent(s): f35b85a

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

Browse files
web/src/components/sidebar/LeftSidebar.tsx CHANGED
@@ -1,8 +1,13 @@
1
  // web/src/components/sidebar/LeftSidebar.tsx
2
  import React, { useEffect, useMemo, useState } from "react";
3
  import { Separator } from "../ui/separator";
 
 
4
 
5
  import { SavedChatSection } from "./SavedChatSection";
 
 
 
6
 
7
  import type {
8
  SavedChat,
@@ -14,10 +19,18 @@ import type {
14
  User,
15
  SavedItem,
16
  } from "../../App";
17
-
18
- import { Button } from "../ui/button";
19
- import { Input } from "../ui/input";
20
- import { Pencil, Check, X } from "lucide-react";
 
 
 
 
 
 
 
 
21
 
22
  type Props = {
23
  learningMode: LearningMode;
@@ -55,10 +68,28 @@ type Props = {
55
  workspaces: Workspace[];
56
 
57
  selectedCourse: string;
58
- availableCourses: any[];
 
 
 
 
 
 
 
59
  };
60
 
61
- function getDisplayName(u: User | null) {
 
 
 
 
 
 
 
 
 
 
 
62
  const anyU = u as any;
63
  return (
64
  anyU?.name ||
@@ -69,22 +100,7 @@ function getDisplayName(u: User | null) {
69
  );
70
  }
71
 
72
- function getCourseDisplayName(selectedCourse: string, availableCourses: any[]) {
73
- if (!selectedCourse) return "—";
74
- const hit =
75
- availableCourses?.find?.(
76
- (c: any) =>
77
- c?.id === selectedCourse ||
78
- c?.courseId === selectedCourse ||
79
- c?.slug === selectedCourse ||
80
- c?.name === selectedCourse ||
81
- c?.title === selectedCourse
82
- ) || null;
83
-
84
- return hit?.name || hit?.title || String(selectedCourse);
85
- }
86
-
87
- function pickField(obj: any, keys: string[]) {
88
  for (const k of keys) {
89
  const v = obj?.[k];
90
  if (v !== undefined && v !== null && String(v).trim() !== "") return v;
@@ -94,8 +110,9 @@ function pickField(obj: any, keys: string[]) {
94
 
95
  export function LeftSidebar(props: Props) {
96
  const {
97
- isLoggedIn,
98
  user,
 
 
99
  groupMembers,
100
  savedChats,
101
  onLoadChat,
@@ -105,44 +122,108 @@ export function LeftSidebar(props: Props) {
105
  workspaces,
106
  selectedCourse,
107
  availableCourses,
 
 
108
  } = props;
109
 
110
  const currentWorkspace = useMemo(
111
- () => workspaces.find((w) => (w as any)?.id === currentWorkspaceId) as any,
112
  [workspaces, currentWorkspaceId]
113
  );
114
 
115
- // ---------- Group Name (editable, local persisted) ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  const groupNameStorageKey = useMemo(
117
  () => `clare_group_name__${currentWorkspaceId}`,
118
  [currentWorkspaceId]
119
  );
120
 
121
- const defaultGroupName =
122
- pickField(currentWorkspace, ["groupName", "group_title", "group_title_name"]) ||
123
- pickField(currentWorkspace, ["name", "title"]) ||
124
- "My Group";
125
-
126
  const [groupName, setGroupName] = useState<string>(defaultGroupName);
127
- const [isEditingGroupName, setIsEditingGroupName] = useState(false);
128
  const [draftGroupName, setDraftGroupName] = useState<string>(defaultGroupName);
129
 
130
  useEffect(() => {
131
- // load from storage first
132
- const stored = typeof window !== "undefined" ? window.localStorage.getItem(groupNameStorageKey) : null;
133
  const name = stored && stored.trim() ? stored : defaultGroupName;
134
-
135
  setGroupName(name);
136
  setDraftGroupName(name);
137
- setIsEditingGroupName(false);
138
- // eslint-disable-next-line react-hooks/exhaustive-deps
139
  }, [groupNameStorageKey, defaultGroupName]);
140
 
141
- const saveGroupName = () => {
142
  const next = (draftGroupName || "").trim() || "My Group";
 
 
143
  setGroupName(next);
144
  setDraftGroupName(next);
145
- setIsEditingGroupName(false);
 
 
 
 
 
 
 
 
 
 
 
146
  try {
147
  window.localStorage.setItem(groupNameStorageKey, next);
148
  } catch {
@@ -152,129 +233,133 @@ export function LeftSidebar(props: Props) {
152
 
153
  const cancelGroupName = () => {
154
  setDraftGroupName(groupName);
155
- setIsEditingGroupName(false);
156
  };
157
 
158
- // ---------- Group # / Instructor / TA (best-effort from workspace) ----------
159
- const groupNumber =
160
- pickField(currentWorkspace, ["groupNumber", "groupNo", "groupIndex", "group_id", "groupId"]) ?? "";
161
 
162
- const instructor =
163
- pickField(currentWorkspace, ["instructor", "teacher", "professor", "instructorName", "teacherName"]) ?? "";
164
 
165
- const ta =
166
- pickField(currentWorkspace, ["ta", "TA", "teachingAssistant", "assistant", "taName"]) ?? "";
167
-
168
- const courseName = useMemo(
169
- () => getCourseDisplayName(selectedCourse, availableCourses),
170
- [selectedCourse, availableCourses]
171
- );
172
 
173
- const displayName = useMemo(() => getDisplayName(user), [user]);
 
 
 
 
174
 
175
  return (
176
- <div className="h-full w-full flex flex-col min-h-0">
177
- {/* ---------- TOP (non-scroll) ---------- */}
178
- <div className="flex-shrink-0 px-4 pt-4 pb-3 space-y-3">
179
  {/* Welcome */}
180
- <div className="text-[13px] text-muted-foreground">Welcome,</div>
181
- <div className="text-[16px] font-semibold leading-tight">{displayName}!</div>
 
 
 
182
 
183
  <Separator className="bg-[#ECECF1]" />
184
 
185
- {/* Class Name */}
186
- <div className="space-y-1">
187
- <div className="text-[12px] text-muted-foreground">Class</div>
188
- <div className="text-[14px] font-medium leading-snug">{courseName}</div>
189
- </div>
190
 
191
- {/* Group # */}
192
- <div className="space-y-1">
193
- <div className="text-[12px] text-muted-foreground">Group #</div>
194
- <div className="text-[14px] font-medium">{String(groupNumber)}</div>
195
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
- {/* Group Name (editable) */}
198
- <div className="space-y-1">
199
- <div className="flex items-center justify-between gap-2">
200
- <div className="text-[12px] text-muted-foreground">Group Name</div>
201
-
202
- {!isEditingGroupName ? (
203
- <button
204
- type="button"
205
- className="inline-flex items-center gap-1 text-[12px] text-muted-foreground hover:text-foreground"
206
- onClick={() => setIsEditingGroupName(true)}
207
- aria-label="Edit group name"
208
- >
209
- <Pencil className="w-3.5 h-3.5" />
210
- </button>
211
- ) : (
212
- <div className="flex items-center gap-1">
213
- <Button
214
- type="button"
215
- size="icon"
216
- variant="ghost"
217
- className="h-7 w-7"
218
- onClick={saveGroupName}
219
- aria-label="Save group name"
220
- >
221
- <Check className="w-4 h-4" />
222
- </Button>
223
  <Button
224
  type="button"
225
- size="icon"
226
- variant="ghost"
227
- className="h-7 w-7"
228
- onClick={cancelGroupName}
229
- aria-label="Cancel edit group name"
230
  >
231
- <X className="w-4 h-4" />
 
232
  </Button>
233
  </div>
234
- )}
235
- </div>
236
 
237
- {!isEditingGroupName ? (
238
- <div className="text-[14px] font-medium leading-snug">{groupName}</div>
239
- ) : (
240
- <Input
241
- value={draftGroupName}
242
- onChange={(e) => setDraftGroupName(e.target.value)}
243
- onKeyDown={(e) => {
244
- if (e.key === "Enter") saveGroupName();
245
- if (e.key === "Escape") cancelGroupName();
246
- }}
247
- className="h-8"
248
- autoFocus
249
- />
250
  )}
251
  </div>
252
 
253
- {/* Group Members */}
254
- <div className="space-y-2">
255
- <div className="text-[12px] text-muted-foreground">Group members</div>
256
- <div className="space-y-1">
257
- {(groupMembers || []).length === 0 ? (
258
- <div className="text-[12px] text-muted-foreground">—</div>
259
- ) : (
260
- groupMembers.map((m: any) => {
261
- const name = m?.name || m?.fullName || m?.username || m?.email || "Member";
262
- return (
263
- <div key={m?.id || name} className="text-[13px] leading-snug">
264
- {name}
265
- </div>
266
- );
267
- })
268
- )}
269
- </div>
270
- </div>
271
  </div>
272
 
273
- <Separator className="flex-shrink-0 bg-[#ECECF1]" />
274
-
275
- {/* ---------- MIDDLE (only scroll area) ---------- */}
276
  <div className="flex-1 min-h-0 overflow-hidden">
277
- <div className="h-full min-h-0 panelScroll px-2 py-2">
278
  <SavedChatSection
279
  isLoggedIn={isLoggedIn}
280
  savedChats={savedChats}
@@ -285,27 +370,51 @@ export function LeftSidebar(props: Props) {
285
  </div>
286
  </div>
287
 
288
- {/* ---------- BOTTOM (fixed, non-scroll) ---------- */}
289
- {(String(instructor || "").trim() || String(ta || "").trim()) ? (
290
- <>
291
- <Separator className="flex-shrink-0 bg-[#ECECF1]" />
292
- <div className="flex-shrink-0 px-4 py-3 space-y-2">
293
- {String(instructor || "").trim() ? (
294
- <div className="space-y-1">
295
- <div className="text-[12px] text-muted-foreground">Instructor</div>
296
- <div className="text-[13px]">{String(instructor)}</div>
297
- </div>
298
- ) : null}
 
 
 
 
 
 
 
 
 
 
 
 
299
 
300
- {String(ta || "").trim() ? (
301
- <div className="space-y-1">
302
- <div className="text-[12px] text-muted-foreground">TA</div>
303
- <div className="text-[13px]">{String(ta)}</div>
304
- </div>
305
- ) : null}
 
 
 
 
 
 
 
 
 
 
 
 
306
  </div>
307
- </>
308
- ) : null}
309
  </div>
310
  );
311
  }
 
1
  // web/src/components/sidebar/LeftSidebar.tsx
2
  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 { Users, Mail, Pencil, Check, X } from "lucide-react";
11
 
12
  import type {
13
  SavedChat,
 
19
  User,
20
  SavedItem,
21
  } from "../../App";
22
+ import type { CourseDirectoryItem } from "../../lib/courseDirectory";
23
+
24
+ /**
25
+ * Behavior:
26
+ * - spaceType === "group" (Team/Group space): show "Group Members (N)" + Invite button + member list (old design).
27
+ * - otherwise (My Space / personal): show simple "Group {groupNo}" + editable "Group {groupName}".
28
+ *
29
+ * Layout:
30
+ * - TOP: Welcome + Course + (Group block: either simple or members panel) => non-scroll
31
+ * - MIDDLE: Saved Chat => only scroll
32
+ * - BOTTOM: Instructor/TA (click -> Gmail compose) => fixed, non-scroll
33
+ */
34
 
35
  type Props = {
36
  learningMode: LearningMode;
 
68
  workspaces: Workspace[];
69
 
70
  selectedCourse: string;
71
+ availableCourses: CourseDirectoryItem[];
72
+
73
+ // optional: if you already have invite flow somewhere, wire it here; otherwise UI stays but does nothing.
74
+ onInviteGroupMembers?: () => void;
75
+
76
+ // optional: if you have backend rename endpoint, wire it here (preferred).
77
+ // if not provided, we fallback to localStorage persistence.
78
+ onRenameGroupName?: (workspaceId: string, newName: string) => Promise<void> | void;
79
  };
80
 
81
+ function gmailComposeLink(email: string, subject?: string, body?: string) {
82
+ const to = `to=${encodeURIComponent(email)}`;
83
+ const su = subject ? `&su=${encodeURIComponent(subject)}` : "";
84
+ const bd = body ? `&body=${encodeURIComponent(body)}` : "";
85
+ return `https://mail.google.com/mail/?view=cm&fs=1&${to}${su}${bd}`;
86
+ }
87
+
88
+ function norm(s: any) {
89
+ return String(s ?? "").trim().toLowerCase();
90
+ }
91
+
92
+ function getUserName(u: User | null) {
93
  const anyU = u as any;
94
  return (
95
  anyU?.name ||
 
100
  );
101
  }
102
 
103
+ function pickAny(obj: any, keys: string[]) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  for (const k of keys) {
105
  const v = obj?.[k];
106
  if (v !== undefined && v !== null && String(v).trim() !== "") return v;
 
110
 
111
  export function LeftSidebar(props: Props) {
112
  const {
 
113
  user,
114
+ isLoggedIn,
115
+ spaceType,
116
  groupMembers,
117
  savedChats,
118
  onLoadChat,
 
122
  workspaces,
123
  selectedCourse,
124
  availableCourses,
125
+ onInviteGroupMembers,
126
+ onRenameGroupName,
127
  } = props;
128
 
129
  const currentWorkspace = useMemo(
130
+ () => workspaces.find((w) => w.id === currentWorkspaceId),
131
  [workspaces, currentWorkspaceId]
132
  );
133
 
134
+ // --------- CourseInfo resolution (same strategy as CourseInfoSection) ---------
135
+ const courseInfo = useMemo((): CourseDirectoryItem | null => {
136
+ const list = Array.isArray(availableCourses) ? availableCourses : [];
137
+
138
+ // 1) selectedCourse: may be id or name
139
+ const selRaw = (selectedCourse || "").trim();
140
+ const sel = norm(selRaw);
141
+ if (sel) {
142
+ const hit =
143
+ list.find((c: any) => norm(c.id) === sel) ||
144
+ list.find((c: any) => norm(c.name) === sel);
145
+ if (hit) return hit;
146
+ }
147
+
148
+ // 2) workspace.courseInfo: may have id/name, sometimes includes instructor/TA already
149
+ const wsCourse = (currentWorkspace as any)?.courseInfo as
150
+ | { id?: string; name?: string; instructor?: any; teachingAssistant?: any }
151
+ | undefined;
152
+
153
+ const wsId = norm(wsCourse?.id);
154
+ if (wsId) {
155
+ return (list.find((c: any) => norm(c.id) === wsId) ?? (wsCourse as any)) as any;
156
+ }
157
+
158
+ const wsName = norm(wsCourse?.name);
159
+ if (wsName) {
160
+ return (list.find((c: any) => norm(c.name) === wsName) ?? (wsCourse as any)) as any;
161
+ }
162
+
163
+ return null;
164
+ }, [availableCourses, currentWorkspace, selectedCourse]);
165
+
166
+ const courseName = useMemo(() => {
167
+ return (courseInfo as any)?.name ?? (selectedCourse || "Course");
168
+ }, [courseInfo, selectedCourse]);
169
+
170
+ // --------- Group number/name (My Space view) ---------
171
+ const groupNo = useMemo(() => {
172
+ const ws: any = currentWorkspace as any;
173
+ return (
174
+ ws?.groupNo ??
175
+ ws?.groupNumber ??
176
+ ws?.groupIndex ??
177
+ ws?.group_id ??
178
+ ws?.groupId ??
179
+ 1
180
+ );
181
+ }, [currentWorkspace]);
182
+
183
+ const defaultGroupName = useMemo(() => {
184
+ const ws: any = currentWorkspace as any;
185
+ return (
186
+ pickAny(ws, ["groupName", "name", "title"]) ||
187
+ "My Group"
188
+ );
189
+ }, [currentWorkspace]);
190
+
191
  const groupNameStorageKey = useMemo(
192
  () => `clare_group_name__${currentWorkspaceId}`,
193
  [currentWorkspaceId]
194
  );
195
 
 
 
 
 
 
196
  const [groupName, setGroupName] = useState<string>(defaultGroupName);
197
+ const [editingGroupName, setEditingGroupName] = useState(false);
198
  const [draftGroupName, setDraftGroupName] = useState<string>(defaultGroupName);
199
 
200
  useEffect(() => {
201
+ const stored =
202
+ typeof window !== "undefined" ? window.localStorage.getItem(groupNameStorageKey) : null;
203
  const name = stored && stored.trim() ? stored : defaultGroupName;
 
204
  setGroupName(name);
205
  setDraftGroupName(name);
206
+ setEditingGroupName(false);
 
207
  }, [groupNameStorageKey, defaultGroupName]);
208
 
209
+ const saveGroupName = async () => {
210
  const next = (draftGroupName || "").trim() || "My Group";
211
+
212
+ // optimistic UI
213
  setGroupName(next);
214
  setDraftGroupName(next);
215
+ setEditingGroupName(false);
216
+
217
+ // preferred: call provided handler (backend)
218
+ if (onRenameGroupName) {
219
+ try {
220
+ await onRenameGroupName(currentWorkspaceId, next);
221
+ return;
222
+ } catch {
223
+ // fallback to localStorage if handler fails
224
+ }
225
+ }
226
+
227
  try {
228
  window.localStorage.setItem(groupNameStorageKey, next);
229
  } catch {
 
233
 
234
  const cancelGroupName = () => {
235
  setDraftGroupName(groupName);
236
+ setEditingGroupName(false);
237
  };
238
 
239
+ // --------- Contacts (fixed bottom) ---------
240
+ const instructorName = (courseInfo as any)?.instructor?.name ?? "N/A";
241
+ const instructorEmail = String((courseInfo as any)?.instructor?.email ?? "").trim();
242
 
243
+ const taName = (courseInfo as any)?.teachingAssistant?.name ?? "N/A";
244
+ const taEmail = String((courseInfo as any)?.teachingAssistant?.email ?? "").trim();
245
 
246
+ const displayName = useMemo(() => getUserName(user), [user]);
 
 
 
 
 
 
247
 
248
+ // Team/Group space detection: we treat "group" as the Team/Group page
249
+ const isTeamSpace = useMemo(() => {
250
+ const st = String(spaceType || "").toLowerCase();
251
+ return st === "group" || st === "team";
252
+ }, [spaceType]);
253
 
254
  return (
255
+ <div className="h-full w-full flex flex-col min-h-0 bg-white">
256
+ {/* ================= TOP (non-scroll) ================= */}
257
+ <div className="flex-shrink-0">
258
  {/* Welcome */}
259
+ <div className="px-4 pt-6 pb-6 space-y-3">
260
+ <div className="text-[34px] leading-tight font-semibold">
261
+ Welcome, {displayName}!
262
+ </div>
263
+ </div>
264
 
265
  <Separator className="bg-[#ECECF1]" />
266
 
267
+ {/* Course + Group block */}
268
+ <div className="px-4 pt-10 pb-10 space-y-4">
269
+ <div className="text-[30px] leading-tight font-semibold">{courseName}</div>
 
 
270
 
271
+ {/* My Space: simple Group No + Group Name (editable) */}
272
+ {!isTeamSpace ? (
273
+ <div className="space-y-3">
274
+ <div className="text-[30px] leading-tight font-semibold">
275
+ Group {String(groupNo)}
276
+ </div>
277
+
278
+ <div className="flex items-center justify-between gap-2">
279
+ {!editingGroupName ? (
280
+ <>
281
+ <div className="text-[22px] leading-tight font-medium">
282
+ Group {groupName}
283
+ </div>
284
+ <button
285
+ type="button"
286
+ className="inline-flex items-center gap-1 text-muted-foreground hover:text-foreground"
287
+ onClick={() => setEditingGroupName(true)}
288
+ aria-label="Edit group name"
289
+ >
290
+ <Pencil className="w-4 h-4" />
291
+ </button>
292
+ </>
293
+ ) : (
294
+ <div className="w-full flex items-center gap-2">
295
+ <Input
296
+ value={draftGroupName}
297
+ onChange={(e) => setDraftGroupName(e.target.value)}
298
+ onKeyDown={(e) => {
299
+ if (e.key === "Enter") saveGroupName();
300
+ if (e.key === "Escape") cancelGroupName();
301
+ }}
302
+ className="h-9"
303
+ autoFocus
304
+ />
305
+ <Button
306
+ type="button"
307
+ size="icon"
308
+ variant="ghost"
309
+ className="h-9 w-9"
310
+ onClick={saveGroupName}
311
+ aria-label="Save group name"
312
+ >
313
+ <Check className="w-4 h-4" />
314
+ </Button>
315
+ <Button
316
+ type="button"
317
+ size="icon"
318
+ variant="ghost"
319
+ className="h-9 w-9"
320
+ onClick={cancelGroupName}
321
+ aria-label="Cancel edit group name"
322
+ >
323
+ <X className="w-4 h-4" />
324
+ </Button>
325
+ </div>
326
+ )}
327
+ </div>
328
+ </div>
329
+ ) : (
330
+ // Team/Group Space: "Group Members (N)" + Invite + list
331
+ <div className="rounded-xl border bg-[#0B0B0C] text-white overflow-hidden">
332
+ <div className="px-4 py-3 flex items-center justify-between">
333
+ <div className="flex items-center gap-2 text-[14px] font-medium">
334
+ <Users className="w-4 h-4 opacity-90" />
335
+ <span>Group Members ({(groupMembers || []).length})</span>
336
+ </div>
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  <Button
339
  type="button"
340
+ variant="secondary"
341
+ className="h-8 px-3 text-[13px] bg-white/10 hover:bg-white/15 text-white border border-white/15"
342
+ onClick={onInviteGroupMembers}
 
 
343
  >
344
+ <Mail className="w-4 h-4 mr-2" />
345
+ Invite
346
  </Button>
347
  </div>
 
 
348
 
349
+ <div className="px-3 pb-3">
350
+ {/* Reuse existing GroupMembers component (old UI) */}
351
+ <GroupMembers members={groupMembers as any} />
352
+ </div>
353
+ </div>
 
 
 
 
 
 
 
 
354
  )}
355
  </div>
356
 
357
+ <Separator className="bg-[#ECECF1]" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  </div>
359
 
360
+ {/* ================= MIDDLE (only scroll) ================= */}
 
 
361
  <div className="flex-1 min-h-0 overflow-hidden">
362
+ <div className="h-full min-h-0 panelScroll px-0 py-0">
363
  <SavedChatSection
364
  isLoggedIn={isLoggedIn}
365
  savedChats={savedChats}
 
370
  </div>
371
  </div>
372
 
373
+ {/* ================= BOTTOM (fixed, non-scroll) ================= */}
374
+ <div className="flex-shrink-0">
375
+ <Separator className="bg-[#ECECF1]" />
376
+ <div className="px-4 py-4 space-y-2 text-[16px]">
377
+ <div className="text-muted-foreground">
378
+ Instructor:&nbsp;
379
+ {instructorEmail ? (
380
+ <a
381
+ href={gmailComposeLink(
382
+ instructorEmail,
383
+ `[Clare] Question about ${courseName}`,
384
+ `Hi ${instructorName},\n\nI have a question about ${courseName}:\n\n(Write your question here)\n\nThanks,\n`
385
+ )}
386
+ target="_blank"
387
+ rel="noopener noreferrer"
388
+ className="text-primary hover:underline"
389
+ >
390
+ {instructorName}
391
+ </a>
392
+ ) : (
393
+ <span className="text-muted-foreground/60">{instructorName}</span>
394
+ )}
395
+ </div>
396
 
397
+ <div className="text-muted-foreground">
398
+ TA:&nbsp;
399
+ {taEmail ? (
400
+ <a
401
+ href={gmailComposeLink(
402
+ taEmail,
403
+ `[Clare] Help request for ${courseName}`,
404
+ `Hi ${taName},\n\nI need help with ${courseName}:\n\n(Write your question here)\n\nThanks,\n`
405
+ )}
406
+ target="_blank"
407
+ rel="noopener noreferrer"
408
+ className="text-primary hover:underline"
409
+ >
410
+ {taName}
411
+ </a>
412
+ ) : (
413
+ <span className="text-muted-foreground/60">{taName}</span>
414
+ )}
415
  </div>
416
+ </div>
417
+ </div>
418
  </div>
419
  );
420
  }