Spaces:
Running
Running
| 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: | |
| {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: | |
| {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> | |
| ); | |
| } | |