SarahXia0405's picture
Update web/src/components/sidebar/CourseInfoSection.tsx
8be522e verified
import React, { useMemo } from "react";
import { Separator } from "../ui/separator";
import type { Workspace, SpaceType } from "../../App";
import type { CourseDirectoryItem } from "../../lib/courseDirectory";
function gmailComposeLink(email: string, subject?: string, body?: string) {
const to = `to=${encodeURIComponent(email)}`;
const su = subject ? `&su=${encodeURIComponent(subject)}` : "";
const bd = body ? `&body=${encodeURIComponent(body)}` : "";
return `https://mail.google.com/mail/?view=cm&fs=1&${to}${su}${bd}`;
}
function norm(s: any) {
return String(s ?? "").trim().toLowerCase();
}
function pickAny(obj: any, keys: string[]) {
for (const k of keys) {
const v = obj?.[k];
if (v !== undefined && v !== null && String(v).trim() !== "") return v;
}
return undefined;
}
function toIntOrFallback(v: any, fb: number) {
const n = Number(v);
if (Number.isFinite(n) && n > 0) return Math.floor(n);
return fb;
}
export function CourseInfoSection({
currentWorkspaceId,
workspaces,
selectedCourse,
availableCourses,
}: {
currentWorkspaceId: string;
workspaces: Workspace[];
selectedCourse: string;
availableCourses: CourseDirectoryItem[];
}) {
const currentWorkspace = useMemo(
() => workspaces.find((w) => w.id === currentWorkspaceId),
[workspaces, currentWorkspaceId]
);
// 判断当前是否是 Group/Team(用于决定是否展示 Group Name/Number)
const isTeamSpace = useMemo(() => {
const ws: any = currentWorkspace as any;
const st = String((ws?.type as SpaceType) || "").toLowerCase();
return st === "group" || st === "team";
}, [currentWorkspace]);
// --------- CourseInfo resolution ---------
const courseInfo = useMemo((): CourseDirectoryItem | null => {
const list = Array.isArray(availableCourses) ? availableCourses : [];
// 1) selectedCourse: 可能是 id / name(大小写/空格不一致也能匹配)
const selRaw = (selectedCourse || "").trim();
const sel = norm(selRaw);
if (sel) {
const hit =
list.find((c: any) => norm(c.id) === sel) ||
list.find((c: any) => norm(c.name) === sel);
if (hit) return hit as any;
}
// 2) workspace.courseInfo: group workspace 的 courseInfo 可能只带 id/name
const wsCourse = (currentWorkspace as any)?.courseInfo as
| { id?: string; name?: string; instructor?: any; teachingAssistant?: any }
| undefined;
const wsId = norm(wsCourse?.id);
if (wsId) return (list.find((c: any) => norm(c.id) === wsId) ?? (wsCourse as any)) as any;
const wsName = norm(wsCourse?.name);
if (wsName) return (list.find((c: any) => norm(c.name) === wsName) ?? (wsCourse as any)) as any;
return null;
}, [availableCourses, currentWorkspace, selectedCourse]);
// ---------- Display fields ----------
const courseName = useMemo(() => {
return (courseInfo as any)?.name ?? (selectedCourse || "Course");
}, [courseInfo, selectedCourse]);
// --------- Group fields (read-only) ---------
const groupName = useMemo(() => {
const ws: any = currentWorkspace as any;
// 优先 groupName,否则退回 workspace 的 name/title
return String(pickAny(ws, ["groupName", "name", "title"]) || "My Group");
}, [currentWorkspace]);
const groupNo = useMemo(() => {
const ws: any = currentWorkspace as any;
const raw = ws?.groupNo ?? ws?.groupNumber ?? ws?.groupIndex ?? ws?.group_id ?? ws?.groupId ?? 1;
return toIntOrFallback(raw, 1);
}, [currentWorkspace]);
// ---------- Fallback:不要静默消失 ----------
if (!courseInfo) {
return (
<div className="w-full">
<div className="px-4 pt-4 pb-3 space-y-2">
<div className="text-xs text-red-600">COURSEINFO ACTIVE (fallback)</div>
<div className="font-semibold text-base">No course matched</div>
<div className="text-xs text-muted-foreground space-y-1">
<div>
selectedCourse: <span className="font-medium">{String(selectedCourse || "")}</span>
</div>
<div>
availableCourses: <span className="font-medium">{availableCourses?.length ?? 0}</span>
</div>
<div>
currentWorkspaceId: <span className="font-medium">{String(currentWorkspaceId)}</span>
</div>
</div>
</div>
<Separator className="bg-[#ECECF1]" />
</div>
);
}
const instructorName = (courseInfo as any)?.instructor?.name ?? "N/A";
const instructorEmail = String((courseInfo as any)?.instructor?.email ?? "").trim();
const taName = (courseInfo as any)?.teachingAssistant?.name ?? "N/A";
const taEmail = String((courseInfo as any)?.teachingAssistant?.email ?? "").trim();
return (
<div className="w-full">
<div className="px-4 pt-4 pb-3 space-y-3">
{/* Course Title */}
<div className="font-semibold text-base">{courseName}</div>
{/* Group fields (read-only, no pencil) */}
{isTeamSpace && (
<div className="space-y-1.5">
<div className="flex items-center justify-between text-[13px]">
<span className="text-muted-foreground">Group Name:</span>
<span className="font-semibold truncate max-w-[60%] text-right">{groupName}</span>
</div>
<div className="flex items-center justify-between text-[13px]">
<span className="text-muted-foreground">Group Number:</span>
<span className="font-semibold">{groupNo}</span>
</div>
</div>
)}
{/* Instructor */}
<div className="text-sm text-muted-foreground">
Instructor:&nbsp;
{instructorEmail ? (
<a
href={gmailComposeLink(
instructorEmail,
`[Clare] Question about ${courseName}`,
`Hi ${instructorName},\n\nI have a question about ${courseName}:\n\n(Write your question here)\n\nThanks,\n`
)}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{instructorName}
</a>
) : (
<span className="text-muted-foreground/60">{instructorName}</span>
)}
</div>
{/* TA */}
<div className="text-sm text-muted-foreground">
TA:&nbsp;
{taEmail ? (
<a
href={gmailComposeLink(
taEmail,
`[Clare] Help request for ${courseName}`,
`Hi ${taName},\n\nI need help with ${courseName}:\n\n(Write your question here)\n\nThanks,\n`
)}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{taName}
</a>
) : (
<span className="text-muted-foreground/60">{taName}</span>
)}
</div>
</div>
<Separator className="bg-[#ECECF1]" />
</div>
);
}