SarahXia0405 commited on
Commit
f1a1861
·
verified ·
1 Parent(s): bbef147

Update web/src/App.tsx

Browse files
Files changed (1) hide show
  1. web/src/App.tsx +69 -25
web/src/App.tsx CHANGED
@@ -156,9 +156,14 @@ function App() {
156
 
157
  const [user, setUser] = useState<User | null>(null);
158
 
159
- const [currentCourseId, setCurrentCourseId] = useState<string>(
160
- () => localStorage.getItem("myspace_selected_course") || "course1"
161
- );
 
 
 
 
 
162
 
163
  const availableCourses: CourseInfo[] = [
164
  {
@@ -393,7 +398,6 @@ function App() {
393
 
394
  // =========================
395
  // ✅ Scheme 1: "My Space" uses Group-like sidebar view model
396
- // - Only affects LeftSidebar rendering (NOT ChatArea / Header logic)
397
  // =========================
398
  const mySpaceCourseInfo = useMemo(() => {
399
  return availableCourses.find((c) => c.id === currentCourseId);
@@ -414,11 +418,8 @@ function App() {
414
  []
415
  );
416
 
417
- // For sidebar only: treat "My Space" as a course/group-like workspace so Group UI blocks can render.
418
  const sidebarWorkspaces: Workspace[] = useMemo(() => {
419
  if (!workspaces?.length) return workspaces;
420
-
421
- // If user is not ready, do not synthesize
422
  if (!mySpaceUserMember) return workspaces;
423
 
424
  return workspaces.map((w) => {
@@ -426,8 +427,6 @@ function App() {
426
 
427
  return {
428
  ...w,
429
- // keep type as "individual" to avoid breaking other list logic,
430
- // but fill the fields that Group UI depends on
431
  category: "course",
432
  courseName: mySpaceCourseInfo?.name || w.courseName || "My Course",
433
  courseInfo: mySpaceCourseInfo,
@@ -436,12 +435,10 @@ function App() {
436
  });
437
  }, [workspaces, mySpaceCourseInfo, mySpaceUserMember, clareMember]);
438
 
439
- // Some sidebars use spaceType to gate rendering; for My Space we present it as "group" in sidebar only.
440
  const sidebarSpaceType: SpaceType = useMemo(() => {
441
  return currentWorkspaceId === "individual" ? "group" : spaceType;
442
  }, [currentWorkspaceId, spaceType]);
443
 
444
- // Sidebar-only "members" list
445
  const sidebarGroupMembers: GroupMember[] = useMemo(() => {
446
  if (currentWorkspaceId === "individual" && mySpaceUserMember) {
447
  return [clareMember, mySpaceUserMember];
@@ -449,34 +446,85 @@ function App() {
449
  return groupMembers;
450
  }, [currentWorkspaceId, mySpaceUserMember, clareMember, groupMembers]);
451
 
 
 
 
 
 
 
452
 
 
 
 
453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
 
 
 
455
  useEffect(() => {
456
  if (!currentWorkspace) return;
457
-
458
- // Group course workspace: courseId 应该由 workspace 决定(单向写入)
459
  if (currentWorkspace.type === "group" && currentWorkspace.category === "course") {
460
  const cid = currentWorkspace.courseInfo?.id;
461
  if (cid && cid !== currentCourseId) setCurrentCourseId(cid);
 
462
  return;
463
  }
464
-
465
- // Individual workspace: 只在 currentCourseId 为空/无效时才从 localStorage hydrate
466
  if (currentWorkspace.type === "individual") {
467
- const saved = localStorage.getItem("myspace_selected_course");
468
- if (saved && saved !== currentCourseId) setCurrentCourseId(saved);
469
- }
470
- }, [currentWorkspaceId, currentWorkspace, currentCourseId]);
 
 
471
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
 
 
 
 
 
 
 
 
 
 
 
473
 
474
  useEffect(() => {
475
  document.documentElement.classList.toggle("dark", isDarkMode);
476
  localStorage.setItem("theme", isDarkMode ? "dark" : "light");
477
  }, [isDarkMode]);
478
 
479
- // ✅ lock outer page scroll (defensive; your index.css already does this)
480
  useEffect(() => {
481
  const prev = document.body.style.overflow;
482
  document.body.style.overflow = "hidden";
@@ -733,10 +781,6 @@ function App() {
733
 
734
  const handleRemoveFile = (file: File) => setUploadedFiles((prev) => prev.filter((u) => u.file !== file));
735
 
736
- // ✅ CRITICAL FIX:
737
- // - use functional setState to avoid stale closure
738
- // - do NOT overwrite newly-added files
739
- // - safely pick the target file inside the updater
740
  const handleFileTypeChange = async (index: number, type: FileType) => {
741
  if (!user) return;
742
 
@@ -1214,7 +1258,7 @@ function App() {
1214
  }
1215
  leftPanelVisible={leftPanelVisible}
1216
  currentCourseId={currentCourseId}
1217
- onCourseChange={setCurrentCourseId}
1218
  availableCourses={availableCourses}
1219
  showReviewBanner={showReviewBanner}
1220
  onReviewActivity={handleReviewActivity}
 
156
 
157
  const [user, setUser] = useState<User | null>(null);
158
 
159
+ // -------------------------
160
+ // Course selection (stable)
161
+ // -------------------------
162
+ const MYSPACE_COURSE_KEY = "myspace_selected_course";
163
+
164
+ const [currentCourseId, setCurrentCourseId] = useState<string>(() => {
165
+ return localStorage.getItem(MYSPACE_COURSE_KEY) || "course1";
166
+ });
167
 
168
  const availableCourses: CourseInfo[] = [
169
  {
 
398
 
399
  // =========================
400
  // ✅ Scheme 1: "My Space" uses Group-like sidebar view model
 
401
  // =========================
402
  const mySpaceCourseInfo = useMemo(() => {
403
  return availableCourses.find((c) => c.id === currentCourseId);
 
418
  []
419
  );
420
 
 
421
  const sidebarWorkspaces: Workspace[] = useMemo(() => {
422
  if (!workspaces?.length) return workspaces;
 
 
423
  if (!mySpaceUserMember) return workspaces;
424
 
425
  return workspaces.map((w) => {
 
427
 
428
  return {
429
  ...w,
 
 
430
  category: "course",
431
  courseName: mySpaceCourseInfo?.name || w.courseName || "My Course",
432
  courseInfo: mySpaceCourseInfo,
 
435
  });
436
  }, [workspaces, mySpaceCourseInfo, mySpaceUserMember, clareMember]);
437
 
 
438
  const sidebarSpaceType: SpaceType = useMemo(() => {
439
  return currentWorkspaceId === "individual" ? "group" : spaceType;
440
  }, [currentWorkspaceId, spaceType]);
441
 
 
442
  const sidebarGroupMembers: GroupMember[] = useMemo(() => {
443
  if (currentWorkspaceId === "individual" && mySpaceUserMember) {
444
  return [clareMember, mySpaceUserMember];
 
446
  return groupMembers;
447
  }, [currentWorkspaceId, mySpaceUserMember, clareMember, groupMembers]);
448
 
449
+ // =========================
450
+ // ✅ Stable course switching logic
451
+ // =========================
452
+
453
+ // Track whether we have hydrated My Space course selection for the current "visit"
454
+ const didHydrateMySpaceRef = useRef(false);
455
 
456
+ // Wrapper: only allow manual course change in My Space (individual)
457
+ const handleCourseChange = (nextCourseId: string) => {
458
+ if (!nextCourseId) return;
459
 
460
+ // If we are inside a group course workspace, course is bound to workspace; ignore manual changes to avoid flicker
461
+ if (currentWorkspace.type === "group" && currentWorkspace.category === "course") {
462
+ return;
463
+ }
464
+
465
+ // In My Space (individual): update state immediately and persist
466
+ setCurrentCourseId(nextCourseId);
467
+ try {
468
+ localStorage.setItem(MYSPACE_COURSE_KEY, nextCourseId);
469
+ } catch {
470
+ // ignore
471
+ }
472
+ };
473
 
474
+ // Sync courseId from group-workspace (authoritative)
475
+ // AND hydrate My Space from localStorage only once per entry (no loops)
476
  useEffect(() => {
477
  if (!currentWorkspace) return;
478
+
479
+ // Group course workspace: force to workspace course id
480
  if (currentWorkspace.type === "group" && currentWorkspace.category === "course") {
481
  const cid = currentWorkspace.courseInfo?.id;
482
  if (cid && cid !== currentCourseId) setCurrentCourseId(cid);
483
+ didHydrateMySpaceRef.current = false; // reset for next time we go back to My Space
484
  return;
485
  }
486
+
487
+ // My Space: hydrate only once when entering My Space
488
  if (currentWorkspace.type === "individual") {
489
+ if (!didHydrateMySpaceRef.current) {
490
+ didHydrateMySpaceRef.current = true;
491
+
492
+ const saved = localStorage.getItem(MYSPACE_COURSE_KEY);
493
+ const valid =
494
+ saved && availableCourses.some((c) => c.id === saved) ? saved : undefined;
495
 
496
+ const next = valid || currentCourseId || "course1";
497
+ if (next !== currentCourseId) setCurrentCourseId(next);
498
+ }
499
+ }
500
+ }, [
501
+ currentWorkspaceId,
502
+ currentWorkspace?.type,
503
+ currentWorkspace?.category,
504
+ currentWorkspace?.courseInfo?.id,
505
+ // include availableCourses so validation works
506
+ availableCourses,
507
+ currentCourseId,
508
+ currentWorkspace,
509
+ ]);
510
 
511
+ // Persist My Space course selection whenever it changes (defensive, keeps FE consistent)
512
+ useEffect(() => {
513
+ if (currentWorkspace?.type !== "individual") return;
514
+ try {
515
+ const prev = localStorage.getItem(MYSPACE_COURSE_KEY);
516
+ if (prev !== currentCourseId) localStorage.setItem(MYSPACE_COURSE_KEY, currentCourseId);
517
+ } catch {
518
+ // ignore
519
+ }
520
+ }, [currentCourseId, currentWorkspace?.type]);
521
 
522
  useEffect(() => {
523
  document.documentElement.classList.toggle("dark", isDarkMode);
524
  localStorage.setItem("theme", isDarkMode ? "dark" : "light");
525
  }, [isDarkMode]);
526
 
527
+ // ✅ lock outer page scroll
528
  useEffect(() => {
529
  const prev = document.body.style.overflow;
530
  document.body.style.overflow = "hidden";
 
781
 
782
  const handleRemoveFile = (file: File) => setUploadedFiles((prev) => prev.filter((u) => u.file !== file));
783
 
 
 
 
 
784
  const handleFileTypeChange = async (index: number, type: FileType) => {
785
  if (!user) return;
786
 
 
1258
  }
1259
  leftPanelVisible={leftPanelVisible}
1260
  currentCourseId={currentCourseId}
1261
+ onCourseChange={handleCourseChange}
1262
  availableCourses={availableCourses}
1263
  showReviewBanner={showReviewBanner}
1264
  onReviewActivity={handleReviewActivity}