From 4c2313cd5df7aa8fd31d5a559c898cab176c82b8 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:13:00 +0000 Subject: [PATCH 1/2] Improve design and UX: discoverability, color linking, rich labels, term selector - Extract shared course color utility (courseColor.ts) - Add colored left border to workspace entries matching calendar blocks - Show instructor + time in section dropdown labels - Show 'Unlock courses to auto-find schedules' hint when all locked - Add term selector dropdown in footer - Add day labels (Mon-Fri) above time picker columns - Add confirmation dialogs for Remove All and Default Schedule - Replace window.prompt/alert with modal for Import Workspace - Fix help modal typos (schuduler, 'If has a course', 'be time') Co-Authored-By: Rahul Chalamala --- src/App.tsx | 32 +++++++++-- src/Planner.test.tsx | 6 +-- src/Planner.tsx | 9 ++-- src/Workspace.test.tsx | 7 ++- src/Workspace.tsx | 120 +++++++++++++++++++++++++++++------------ src/css/planner.css | 8 +++ src/lib/courseColor.ts | 16 ++++++ 7 files changed, 148 insertions(+), 50 deletions(-) create mode 100644 src/lib/courseColor.ts diff --git a/src/App.tsx b/src/App.tsx index 2e479f3..e8a6f64 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, createContext } from "react"; -import { Routes, Route, Navigate, useParams, Link } from "react-router"; +import { Routes, Route, Navigate, useParams, useNavigate, Link } from "react-router"; import Planner from "./Planner"; import WorkspacePanel from "./Workspace"; import Modal from "./Modal"; @@ -159,15 +159,15 @@ function TermPage() { the section number will not be changed.

- You can also limit sections be time. Above the calendar, you can + You can also limit sections by time. Above the calendar, you can change the allowed time range for any day of the week. The course scheduler should respect these times, and it will not generate arrangements with courses that start before the first time or end - after the second. Note: If has a course doesn't have a time + after the second. Note: If a course doesn't have a time (marked as A), then the scheduler will leave it blank.

- We hope that this course schuduler makes your life easier! You can + We hope that this course scheduler makes your life easier! You can find the source code{" "} , &{" "}

-

Current term: {term}

+ @@ -289,6 +289,28 @@ function App() { /* Shared components */ /* ------------------------------------------------------------------ */ +function TermSelector({ currentTerm }: { currentTerm: string }) { + const navigate = useNavigate(); + const supportedTerms = getSupportedTermPaths(); + + return ( +

+ Term:{" "} + +

+ ); +} + function Hyperlink(props: { href: string; text: string }) { return ( { it("renders the calendar without errors when no courses are added", () => { renderPlanner(); - // react-big-calendar renders day headers: Mon, Tue, Wed, Thu, Fri - expect(screen.getByText("Mon")).toBeInTheDocument(); - expect(screen.getByText("Fri")).toBeInTheDocument(); + // react-big-calendar renders day headers and time-picker labels: Mon, Tue, Wed, Thu, Fri + expect(screen.getAllByText("Mon").length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText("Fri").length).toBeGreaterThanOrEqual(1); }); it("renders time pickers for 5 weekdays", () => { diff --git a/src/Planner.tsx b/src/Planner.tsx index 24a6b05..8111552 100644 --- a/src/Planner.tsx +++ b/src/Planner.tsx @@ -4,6 +4,7 @@ import { Calendar, dayjsLocalizer, Views } from "react-big-calendar"; import dayjs from "dayjs"; import Flatpickr from "react-flatpickr"; import { parseTimes } from "./lib/time"; +import { getCourseColorHSL } from "./lib/courseColor"; import { CourseStorage, DateData } from "./types"; import "react-big-calendar/lib/css/react-big-calendar.css"; @@ -61,12 +62,9 @@ function Planner() { /** Hashes id to proper color and styling for calendar items */ const eventStyleGetter = (event: DateData) => { - const hue = ((event.id * 1.4269) % 1.0) * 360; - const sat = (((event.id * 1.7234) % 0.2) + 0.5) * 100; - return { style: { - backgroundColor: `hsl(${hue}, ${sat}%, 70%)`, + backgroundColor: getCourseColorHSL(event.id), cursor: "pointer", borderStyle: "none", borderRadius: "4px", @@ -77,9 +75,10 @@ function Planner() { return (
- {[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:

+