- {[0, 1, 2, 3, 4].map((idx) => {
+ {["Mon", "Tue", "Wed", "Thu", "Fri"].map((day, idx) => {
return (
+
{day}
{
expect(screen.getByText("1/2")).toBeInTheDocument();
});
- it("shows 'All sections set' when all courses are locked", () => {
+ it("shows unlock hint when all courses are locked", () => {
const courses = [makeCourseStorage(1, { locked: true })];
renderWorkspace({ courses });
- expect(screen.getByText(/all sections set/i)).toBeInTheDocument();
+ expect(screen.getByText(/unlock courses/i)).toBeInTheDocument();
});
it("calls prevArrangement and nextArrangement on arrow clicks", async () => {
@@ -296,7 +296,10 @@ describe("Workspace", () => {
const courses = [makeCourseStorage(1)];
const { mockState } = renderWorkspace({ courses });
+ // Mock window.confirm to return true
+ const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
await user.click(screen.getByText("Remove All"));
expect(mockState.setCourses).toHaveBeenCalledWith([]);
+ confirmSpy.mockRestore();
});
});
diff --git a/src/Workspace.tsx b/src/Workspace.tsx
index 39f15c4..990995a 100644
--- a/src/Workspace.tsx
+++ b/src/Workspace.tsx
@@ -1,4 +1,4 @@
-import { useContext, useEffect, useMemo, useState } from "react";
+import { useContext, useEffect, useMemo, useRef, useState } from "react";
import Modal, { useModal } from "./Modal";
import Select from "react-select";
import { SingleValue } from "react-select";
@@ -27,6 +27,7 @@ import { Collapse, IconButton, Switch } from "@mui/material";
import { UnfoldLess, UnfoldMore } from "@mui/icons-material";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { exportICS } from "./lib/ics";
+import { getCourseColorHSL } from "./lib/courseColor";
import {
CourseData,
CourseIndex,
@@ -113,7 +114,13 @@ function SectionDropdown(props: { course: CourseStorage }) {
}
onChange={onChange}
options={course.courseData.sections}
- getOptionLabel={(section) => `${section.number}`}
+ getOptionLabel={(section) => {
+ const time = section.times === "A" ? "TBD" : section.times;
+ const parts = [`${section.number}`];
+ if (section.instructor) parts.push(section.instructor);
+ parts.push(time);
+ return parts.join(" \u2014 ");
+ }}
isOptionSelected={(section) =>
course.sectionId !== null
? section.number ===
@@ -200,7 +207,10 @@ function WorkspaceEntry(props: WorkspaceEntryProps) {
}`}
ref={provided.innerRef}
{...provided.draggableProps}
- style={provided.draggableProps.style}
+ style={{
+ ...provided.draggableProps.style,
+ borderLeft: `4px solid ${getCourseColorHSL(course.courseData.id)}`,
+ }}
>
-
All sections set.
+
+ {state.courses.length > 0
+ ? "All sections locked. Unlock courses to auto-find non-conflicting schedules."
+ : "All sections set."}
+
);
} else if (total === 0) {
@@ -475,28 +489,57 @@ export default function Workspace({ term }: { term: string }) {
);
});
- const importWorkspace = () => {
- const code = prompt("Copy in the workspace code.") || "";
- if (code === "") {
- return;
- }
- try {
- const shortened = JSON.parse(window.atob(code));
- const courses: CourseStorageShort[] = [];
- for (let i = 0; i * 4 < shortened.length; i++) {
- courses.push({
- courseId: shortened[i * 4],
- enabled: shortened[i * 4 + 1],
- locked: shortened[i * 4 + 2],
- sectionId: shortened[i * 4 + 3],
- });
+ const [importError, setImportError] = useState(null);
+ const importInputRef = useRef(null);
+
+ const [openImportModal, importModal] = useModal(() => {
+ const handleImport = () => {
+ const code = importInputRef.current?.value.trim() || "";
+ if (code === "") {
+ return;
}
- const lengthened = lengthenCourses(courses, indexedCourses);
- state.setCourses(lengthened);
- } catch {
- alert("Error importing workspace.");
- }
- };
+ try {
+ const shortened = JSON.parse(window.atob(code));
+ const courses: CourseStorageShort[] = [];
+ for (let i = 0; i * 4 < shortened.length; i++) {
+ courses.push({
+ courseId: shortened[i * 4],
+ enabled: shortened[i * 4 + 1],
+ locked: shortened[i * 4 + 2],
+ sectionId: shortened[i * 4 + 3],
+ });
+ }
+ const lengthened = lengthenCourses(courses, indexedCourses);
+ state.setCourses(lengthened);
+ setImportError(null);
+ } catch {
+ setImportError("Invalid workspace code. Please check and try again.");
+ }
+ };
+
+ return (
+
+
Paste your workspace code:
+
+ {importError && (
+
{importError}
+ )}
+
+ Import
+
+
+ );
+ });
function onDragEnd(result: DropResult) {
if (
@@ -514,6 +557,7 @@ export default function Workspace({ term }: { term: string }) {
return (
{exportModal}
+ {importModal}
Choose Workspace...
-
state.setCourses([])} />
+ {
+ if (state.courses.length === 0 || window.confirm("Remove all courses from this workspace?")) {
+ state.setCourses([]);
+ }
+ }} />
{
- state.setCourses(
- // Change based on term
- DEFAULT_COURSES[term.substring(0, 2)].map((name) => ({
- ...getCourse(name, indexedCourses)!,
- enabled: true,
- locked: true,
- })),
- );
+ if (state.courses.length === 0 || window.confirm("Replace current courses with the default schedule?")) {
+ state.setCourses(
+ // Change based on term
+ (DEFAULT_COURSES[term.substring(0, 2)] ?? []).map((name) => ({
+ ...getCourse(name, indexedCourses)!,
+ enabled: true,
+ locked: true,
+ })),
+ );
+ }
}}
/>
-
+
{
const icsContent = exportICS(term, state.courses);
diff --git a/src/css/planner.css b/src/css/planner.css
index 57818a1..e505014 100644
--- a/src/css/planner.css
+++ b/src/css/planner.css
@@ -11,6 +11,14 @@
flex-direction: column;
row-gap: 8px;
max-width: 4rem;
+ align-items: center;
+}
+
+.time-picker-label {
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-align: center;
+ color: #737373;
}
.time-picker > input {
diff --git a/src/lib/courseColor.ts b/src/lib/courseColor.ts
new file mode 100644
index 0000000..6e06371
--- /dev/null
+++ b/src/lib/courseColor.ts
@@ -0,0 +1,16 @@
+/**
+ * Shared color utility for deterministic course coloring.
+ * Used by both the calendar (Planner) and workspace (WorkspaceEntry)
+ * to visually link course cards to their calendar blocks.
+ */
+
+export function getCourseColor(courseId: number): { hue: number; sat: number } {
+ const hue = ((courseId * 1.4269) % 1.0) * 360;
+ const sat = (((courseId * 1.7234) % 0.2) + 0.5) * 100;
+ return { hue, sat };
+}
+
+export function getCourseColorHSL(courseId: number, lightness = 70): string {
+ const { hue, sat } = getCourseColor(courseId);
+ return `hsl(${hue}, ${sat}%, ${lightness}%)`;
+}
From ed25cae2d037fe50b74388d166533c2596efa449 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 11 Mar 2026 20:17:56 +0000
Subject: [PATCH 2/2] Address review feedback: fix null crash in Default
Schedule, close import modal on success, clear error on reopen
Co-Authored-By: Rahul Chalamala
---
src/Workspace.tsx | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/src/Workspace.tsx b/src/Workspace.tsx
index 990995a..4f620f7 100644
--- a/src/Workspace.tsx
+++ b/src/Workspace.tsx
@@ -492,7 +492,7 @@ export default function Workspace({ term }: { term: string }) {
const [importError, setImportError] = useState(null);
const importInputRef = useRef(null);
- const [openImportModal, importModal] = useModal(() => {
+ const [openImportModalRaw, importModal] = useModal(({ onClose }) => {
const handleImport = () => {
const code = importInputRef.current?.value.trim() || "";
if (code === "") {
@@ -512,6 +512,7 @@ export default function Workspace({ term }: { term: string }) {
const lengthened = lengthenCourses(courses, indexedCourses);
state.setCourses(lengthened);
setImportError(null);
+ onClose();
} catch {
setImportError("Invalid workspace code. Please check and try again.");
}
@@ -541,6 +542,11 @@ export default function Workspace({ term }: { term: string }) {
);
});
+ const openImportModal = () => {
+ setImportError(null);
+ openImportModalRaw();
+ };
+
function onDragEnd(result: DropResult) {
if (
!result.destination ||
@@ -652,11 +658,14 @@ export default function Workspace({ term }: { term: string }) {
if (state.courses.length === 0 || window.confirm("Replace current courses with the default schedule?")) {
state.setCourses(
// Change based on term
- (DEFAULT_COURSES[term.substring(0, 2)] ?? []).map((name) => ({
- ...getCourse(name, indexedCourses)!,
- enabled: true,
- locked: true,
- })),
+ (DEFAULT_COURSES[term.substring(0, 2)] ?? [])
+ .map((name) => getCourse(name, indexedCourses))
+ .filter((c): c is CourseStorage => c !== null)
+ .map((c) => ({
+ ...c,
+ enabled: true,
+ locked: true,
+ })),
);
}
}}