Spaces:
Sleeping
Sleeping
Update web/src/App.tsx
Browse files- 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 |
-
|
| 160 |
-
|
| 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:
|
| 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 |
-
//
|
| 466 |
if (currentWorkspace.type === "individual") {
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
|
|
|
|
|
|
| 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
|
| 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={
|
| 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}
|