SarahXia0405 commited on
Commit
c4c4e16
·
verified ·
1 Parent(s): 8fdae75

Update web/src/components/LeftSidebar.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/LeftSidebar.tsx +87 -41
web/src/components/LeftSidebar.tsx CHANGED
@@ -3,7 +3,8 @@ import React, { useMemo, useState } from "react";
3
  import { Button } from "./ui/button";
4
  import { Input } from "./ui/input";
5
  import { Separator } from "./ui/separator";
6
- import { Bookmark, Trash2, Edit2, Check, X as XIcon } from "lucide-react";
 
7
 
8
  import type {
9
  LearningMode,
@@ -49,7 +50,6 @@ type Props = {
49
  onLoadChat: (chat: SavedChat) => void;
50
  onDeleteSavedChat: (id: string) => void;
51
 
52
- // ✅ NEW in your App.tsx already
53
  onRenameSavedChat: (id: string, newTitle: string) => void;
54
 
55
  currentWorkspaceId: string;
@@ -72,7 +72,17 @@ function formatSub(ts: any) {
72
  function buildMailto(email: string, subject: string, body?: string) {
73
  const s = encodeURIComponent(subject);
74
  const b = body ? `&body=${encodeURIComponent(body)}` : "";
75
- return `mailto:${encodeURIComponent(email)}?subject=${s}${b}`;
 
 
 
 
 
 
 
 
 
 
76
  }
77
 
78
  function CourseInfoHeader({
@@ -80,46 +90,91 @@ function CourseInfoHeader({
80
  }: {
81
  course: CourseInfo;
82
  }) {
83
- const instructorHref = buildMailto(
84
  course.instructor.email,
85
  `[Clare] Question about ${course.name}`,
86
  `Hi ${course.instructor.name},\n\nI have a question about ${course.name}:\n\n(Write your question here)\n\nThanks,\n`
87
  );
88
 
89
- const taHref = buildMailto(
90
  course.teachingAssistant.email,
91
  `[Clare] Help request for ${course.name}`,
92
  `Hi ${course.teachingAssistant.name},\n\nI need help with ${course.name}:\n\n(Write your question here)\n\nThanks,\n`
93
  );
94
 
95
- return (
96
- <div className="px-4 pt-4 pb-3">
97
- <div className="space-y-1">
98
- <div className="text-base font-semibold text-foreground truncate">{course.name}</div>
99
 
100
- <div className="text-sm text-muted-foreground">
101
- Instructor:{" "}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  <a
103
- href={instructorHref}
104
  className="text-primary hover:underline"
105
- title={`Email ${course.instructor.name}`}
106
- onClick={(e) => e.stopPropagation()}
 
 
 
 
 
107
  >
108
- {course.instructor.name}
109
  </a>
110
  </div>
111
 
112
- <div className="text-sm text-muted-foreground">
113
- TA:{" "}
114
- <a
115
- href={taHref}
116
- className="text-primary hover:underline"
117
- title={`Email ${course.teachingAssistant.name}`}
118
- onClick={(e) => e.stopPropagation()}
119
- >
120
- {course.teachingAssistant.name}
121
- </a>
122
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  </div>
124
  </div>
125
  );
@@ -139,9 +194,6 @@ export function LeftSidebar(props: Props) {
139
 
140
  // =========================
141
  // Course info must ALWAYS show
142
- // Logic kept consistent with App.tsx:
143
- // - If workspace is group/course and has courseInfo -> use it
144
- // - Else fallback to selectedCourse (My Space) -> availableCourses match
145
  // =========================
146
  const currentCourseInfo: CourseInfo | null = useMemo(() => {
147
  const ws = workspaces?.find((w) => w.id === currentWorkspaceId);
@@ -159,7 +211,7 @@ export function LeftSidebar(props: Props) {
159
  }
160
  }
161
 
162
- // individual workspace (or anything else): match selectedCourse id
163
  const byId = availableCourses?.find((c) => c.id === selectedCourse);
164
  return byId || (availableCourses?.[0] ?? null);
165
  }, [availableCourses, currentWorkspaceId, selectedCourse, workspaces]);
@@ -197,19 +249,15 @@ export function LeftSidebar(props: Props) {
197
 
198
  return (
199
  <div className="h-full w-full flex flex-col min-h-0">
200
- {/* =========================
201
- 0) Course Info (ALWAYS)
202
- ========================= */}
203
  {currentCourseInfo ? <CourseInfoHeader course={currentCourseInfo} /> : null}
204
 
205
- {/* Divider between course info and Saved Chat section */}
206
  <div className="px-4 flex-shrink-0">
207
  <Separator />
208
  </div>
209
 
210
- {/* =========================
211
- 1) Saved Chat header
212
- ========================= */}
213
  <div className="flex-shrink-0 px-4 pt-4 pb-3">
214
  <div className="flex items-center gap-2">
215
  <Bookmark className="h-4 w-4" />
@@ -217,9 +265,7 @@ export function LeftSidebar(props: Props) {
217
  </div>
218
  </div>
219
 
220
- {/* =========================
221
- 2) Saved Chat list
222
- ========================= */}
223
  <div className="flex-1 min-h-0 overflow-y-auto px-4 pb-3 space-y-3">
224
  {sortedChats.length === 0 ? (
225
  <div className="text-sm text-muted-foreground text-center py-10">
@@ -330,7 +376,7 @@ export function LeftSidebar(props: Props) {
330
  )}
331
  </div>
332
 
333
- {/* Optional footer placeholder (keeps layout stable) */}
334
  <div className="flex-shrink-0 px-4 pb-4" />
335
  </div>
336
  );
 
3
  import { Button } from "./ui/button";
4
  import { Input } from "./ui/input";
5
  import { Separator } from "./ui/separator";
6
+ import { Bookmark, Trash2, Edit2, Check, X as XIcon, Copy } from "lucide-react";
7
+ import { toast } from "sonner";
8
 
9
  import type {
10
  LearningMode,
 
50
  onLoadChat: (chat: SavedChat) => void;
51
  onDeleteSavedChat: (id: string) => void;
52
 
 
53
  onRenameSavedChat: (id: string, newTitle: string) => void;
54
 
55
  currentWorkspaceId: string;
 
72
  function buildMailto(email: string, subject: string, body?: string) {
73
  const s = encodeURIComponent(subject);
74
  const b = body ? `&body=${encodeURIComponent(body)}` : "";
75
+ // IMPORTANT: do not encode the whole string, only params
76
+ return `mailto:${email}?subject=${s}${b}`;
77
+ }
78
+
79
+ async function copyToClipboard(text: string) {
80
+ try {
81
+ await navigator.clipboard.writeText(text);
82
+ toast.success("Email copied");
83
+ } catch {
84
+ toast.error("Copy failed");
85
+ }
86
  }
87
 
88
  function CourseInfoHeader({
 
90
  }: {
91
  course: CourseInfo;
92
  }) {
93
+ const instructorMailto = buildMailto(
94
  course.instructor.email,
95
  `[Clare] Question about ${course.name}`,
96
  `Hi ${course.instructor.name},\n\nI have a question about ${course.name}:\n\n(Write your question here)\n\nThanks,\n`
97
  );
98
 
99
+ const taMailto = buildMailto(
100
  course.teachingAssistant.email,
101
  `[Clare] Help request for ${course.name}`,
102
  `Hi ${course.teachingAssistant.name},\n\nI need help with ${course.name}:\n\n(Write your question here)\n\nThanks,\n`
103
  );
104
 
105
+ // Use <a href="mailto:..."> (works best), and also force navigation via location as fallback.
106
+ const openMail = (mailto: string) => {
107
+ window.location.href = mailto;
108
+ };
109
 
110
+ const Row = ({
111
+ label,
112
+ name,
113
+ email,
114
+ mailto,
115
+ }: {
116
+ label: string;
117
+ name: string;
118
+ email: string;
119
+ mailto: string;
120
+ }) => {
121
+ return (
122
+ <div className="flex items-center justify-between gap-2 text-sm text-muted-foreground">
123
+ <div className="min-w-0">
124
+ <span>{label}: </span>
125
+
126
+ {/* Primary: anchor mailto (should open default mail app) */}
127
  <a
128
+ href={mailto}
129
  className="text-primary hover:underline"
130
+ title={`Email ${name}`}
131
+ onClick={(e) => {
132
+ // Some browsers block navigation if event is prevented; we do NOT prevent default.
133
+ // We add an explicit location jump as a fallback.
134
+ // Note: no preventDefault.
135
+ openMail(mailto);
136
+ }}
137
  >
138
+ {name}
139
  </a>
140
  </div>
141
 
142
+ {/* Secondary: copy email (always works) */}
143
+ <Button
144
+ type="button"
145
+ variant="ghost"
146
+ size="icon"
147
+ className="h-7 w-7 flex-shrink-0"
148
+ title="Copy email"
149
+ onClick={(e) => {
150
+ e.stopPropagation();
151
+ copyToClipboard(email);
152
+ }}
153
+ >
154
+ <Copy className="h-3.5 w-3.5" />
155
+ </Button>
156
+ </div>
157
+ );
158
+ };
159
+
160
+ return (
161
+ <div className="px-4 pt-4 pb-3 flex-shrink-0">
162
+ <div className="space-y-2">
163
+ <div className="text-base font-semibold text-foreground truncate">{course.name}</div>
164
+
165
+ <Row
166
+ label="Instructor"
167
+ name={course.instructor.name}
168
+ email={course.instructor.email}
169
+ mailto={instructorMailto}
170
+ />
171
+
172
+ <Row
173
+ label="TA"
174
+ name={course.teachingAssistant.name}
175
+ email={course.teachingAssistant.email}
176
+ mailto={taMailto}
177
+ />
178
  </div>
179
  </div>
180
  );
 
194
 
195
  // =========================
196
  // Course info must ALWAYS show
 
 
 
197
  // =========================
198
  const currentCourseInfo: CourseInfo | null = useMemo(() => {
199
  const ws = workspaces?.find((w) => w.id === currentWorkspaceId);
 
211
  }
212
  }
213
 
214
+ // individual workspace: match selectedCourse id
215
  const byId = availableCourses?.find((c) => c.id === selectedCourse);
216
  return byId || (availableCourses?.[0] ?? null);
217
  }, [availableCourses, currentWorkspaceId, selectedCourse, workspaces]);
 
249
 
250
  return (
251
  <div className="h-full w-full flex flex-col min-h-0">
252
+ {/* 0) Course Info (fixed, not scroll) */}
 
 
253
  {currentCourseInfo ? <CourseInfoHeader course={currentCourseInfo} /> : null}
254
 
255
+ {/* Divider between Info and Saved Chat (fixed) */}
256
  <div className="px-4 flex-shrink-0">
257
  <Separator />
258
  </div>
259
 
260
+ {/* 1) Saved Chat header (fixed, not scroll) */}
 
 
261
  <div className="flex-shrink-0 px-4 pt-4 pb-3">
262
  <div className="flex items-center gap-2">
263
  <Bookmark className="h-4 w-4" />
 
265
  </div>
266
  </div>
267
 
268
+ {/* 2) Saved Chat list (ONLY this scrolls) */}
 
 
269
  <div className="flex-1 min-h-0 overflow-y-auto px-4 pb-3 space-y-3">
270
  {sortedChats.length === 0 ? (
271
  <div className="text-sm text-muted-foreground text-center py-10">
 
376
  )}
377
  </div>
378
 
379
+ {/* footer spacer */}
380
  <div className="flex-shrink-0 px-4 pb-4" />
381
  </div>
382
  );