From 67ccab6de7222a38501e975a6f4dd7b928e0db75 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 22 Jun 2026 06:53:39 +0000
Subject: [PATCH 1/3] improve calendar event display: show course + location,
hide time
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
---
src/Planner.tsx | 45 ++++++++++++++++++++++++++++++++++--------
src/css/planner.css | 20 ++++++++++++++++++-
src/react-app-env.d.ts | 1 +
3 files changed, 57 insertions(+), 9 deletions(-)
diff --git a/src/Planner.tsx b/src/Planner.tsx
index f56d6ee..1612af2 100644
--- a/src/Planner.tsx
+++ b/src/Planner.tsx
@@ -13,6 +13,22 @@ import "./css/planner.css";
const hasWeekendCourse = false;
+/** Extract a concise room label from the raw newline-separated locations string. */
+function shortLocation(raw: string): string {
+ if (!raw) return "";
+ const lines = raw
+ .split("\n")
+ .map((l) => l.trim())
+ .filter(Boolean);
+ // room entries look like "387 LINDE", "103 DWN", "106 ANB"
+ const rooms = lines.filter((l) => /^\d+\s+[A-Z]/.test(l));
+ if (rooms.length > 0) return rooms[0];
+ // fall back to building codes (short all-caps tokens)
+ const buildings = lines.filter((l) => /^[A-Z]{2,}$/.test(l));
+ if (buildings.length > 0) return buildings[0];
+ return "";
+}
+
function formatTime(date: Date): string {
return `${String(date.getHours()).padStart(2, "0")}:${String(
date.getMinutes(),
@@ -66,7 +82,8 @@ function CourseToDates(courses: CourseStorage[]): DateData[] {
for (const interval of day) {
dates.push({
id: course.courseData.id,
- title: course.courseData.number + " Section " + section.number,
+ title: course.courseData.number + " ยง" + section.number,
+ location: shortLocation(section.locations),
start: interval!.start,
end: interval!.end,
courseData: course.courseData,
@@ -103,14 +120,26 @@ function courseColors(id: number) {
};
}
+function escapeHtml(s: string): string {
+ return s.replace(/&/g, "&").replace(//g, ">");
+}
+
function toExternalEvents(calEvents: DateData[]) {
- return calEvents.map((event, idx) => ({
- id: `${event.id}-${idx}`,
- title: event.title,
- start: toZonedDateTime(event.start),
- end: toZonedDateTime(event.end),
- calendarId: `course-${event.id}`,
- }));
+ return calEvents.map((event, idx) => {
+ const loc = event.location;
+ const html = loc
+ ? `${escapeHtml(event.title)}
${escapeHtml(loc)}`
+ : `${escapeHtml(event.title)}`;
+
+ return {
+ id: `${event.id}-${idx}`,
+ title: event.title,
+ start: toZonedDateTime(event.start),
+ end: toZonedDateTime(event.end),
+ calendarId: `course-${event.id}`,
+ _customContent: { timeGrid: html },
+ };
+ });
}
function ScheduleCalendar({ calEvents }: { calEvents: DateData[] }) {
diff --git a/src/css/planner.css b/src/css/planner.css
index 56bc45d..ea0ee1c 100644
--- a/src/css/planner.css
+++ b/src/css/planner.css
@@ -17,7 +17,7 @@
border-radius: 4px;
width: 100%;
min-width: 0;
- max-width: 4.5rem;
+ max-width: 5.5rem;
text-align: center;
background-color: white;
font-size: 0.875rem;
@@ -27,3 +27,21 @@
.planner-time-input::-webkit-calendar-picker-indicator {
display: none;
}
+
+/* custom event content */
+.sx-event-title {
+ font-weight: 600;
+ font-size: 0.75rem;
+ line-height: 1.2;
+}
+
+.sx-event-location {
+ font-size: 0.65rem;
+ opacity: 0.8;
+ line-height: 1.2;
+}
+
+/* hide the default time label Schedule-X renders inside events */
+.sx__time-grid-event .sx__time-grid-event-time {
+ display: none;
+}
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
index 0e44606..263f714 100644
--- a/src/react-app-env.d.ts
+++ b/src/react-app-env.d.ts
@@ -24,6 +24,7 @@ interface CourseData {
interface DateData {
id: number;
title: string;
+ location: string;
start: Date;
end: Date;
courseData: CourseData;
From ec39000579e641b93263e2789a4c2b8644035524 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 22 Jun 2026 06:58:31 +0000
Subject: [PATCH 2/3] fix shortLocation regex to match basement rooms (B280
MRE, 115c BAX, etc.)
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
---
src/Planner.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Planner.tsx b/src/Planner.tsx
index 1612af2..c89a5a3 100644
--- a/src/Planner.tsx
+++ b/src/Planner.tsx
@@ -20,8 +20,8 @@ function shortLocation(raw: string): string {
.split("\n")
.map((l) => l.trim())
.filter(Boolean);
- // room entries look like "387 LINDE", "103 DWN", "106 ANB"
- const rooms = lines.filter((l) => /^\d+\s+[A-Z]/.test(l));
+ // room entries: "387 LINDE", "B280 MRE", "115c BAX", "240A CNRB"
+ const rooms = lines.filter((l) => /^[A-Z]?\d+\w*\s+[A-Z]/.test(l));
if (rooms.length > 0) return rooms[0];
// fall back to building codes (short all-caps tokens)
const buildings = lines.filter((l) => /^[A-Z]{2,}$/.test(l));
From 66592570293302aa3fb9717de3c3ae28e168be36 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 22 Jun 2026 07:11:09 +0000
Subject: [PATCH 3/3] replace native time inputs with MUI TimePicker (24hr,
consistent design system)
Co-Authored-By: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
---
package-lock.json | 146 ++++++++++++++++++++++++++++++++++++++++++++
package.json | 2 +
src/Planner.tsx | 38 +++++++-----
src/css/planner.css | 16 -----
src/index.tsx | 6 +-
5 files changed, 176 insertions(+), 32 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 147eb61..0e3baca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,11 +14,13 @@
"@hello-pangea/dnd": "^18.0.1",
"@mui/icons-material": "^9.1.0",
"@mui/material": "^9.1.0",
+ "@mui/x-date-pickers": "^9.6.0",
"@preact/signals": "^2.9.1",
"@schedule-x/calendar": "^4.6.0",
"@schedule-x/events-service": "^4.6.0",
"@schedule-x/react": "^4.1.0",
"@schedule-x/theme-default": "^4.6.0",
+ "dayjs": "^1.11.21",
"fzf": "^0.5.1",
"ics": "^3.12.0",
"lz-string": "^1.5.0",
@@ -42,6 +44,9 @@
"vite-plugin-svgr": "^5.2.0",
"vite-tsconfig-paths": "^6.1.1",
"wrangler": "4.98.0"
+ },
+ "engines": {
+ "node": ">=20.19"
}
},
"node_modules/@babel/code-frame": {
@@ -283,6 +288,28 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@base-ui/utils": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.2.9.tgz",
+ "integrity": "sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.2",
+ "@floating-ui/utils": "^0.2.11",
+ "reselect": "^5.1.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^17 || ^18 || ^19",
+ "react": "^17 || ^18 || ^19",
+ "react-dom": "^17 || ^18 || ^19"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@cloudflare/kv-asset-handler": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
@@ -1051,6 +1078,12 @@
"node": ">=18"
}
},
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
"node_modules/@formkit/auto-animate": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.9.0.tgz",
@@ -1843,6 +1876,96 @@
}
}
},
+ "node_modules/@mui/x-date-pickers": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-9.6.0.tgz",
+ "integrity": "sha512-Cdk0qkdfxjQtDAnrQdrkCCGyXifjmqzzoDnnoPPxLz5915BWW33cAG9RcZFUAupRaUuh3i6QE8C5oUA9Mrx+1A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.7",
+ "@mui/utils": "^9.0.1",
+ "@mui/x-internals": "^9.6.0",
+ "@types/react-transition-group": "^4.4.12",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^7.3.0 || ^9.0.0",
+ "@mui/system": "^7.3.0 || ^9.0.0",
+ "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
+ "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
+ "dayjs": "^1.10.7",
+ "luxon": "^3.0.2",
+ "moment": "^2.29.4",
+ "moment-hijri": "^2.1.2 || ^3.0.0",
+ "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "date-fns": {
+ "optional": true
+ },
+ "date-fns-jalali": {
+ "optional": true
+ },
+ "dayjs": {
+ "optional": true
+ },
+ "luxon": {
+ "optional": true
+ },
+ "moment": {
+ "optional": true
+ },
+ "moment-hijri": {
+ "optional": true
+ },
+ "moment-jalaali": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-internals": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.6.0.tgz",
+ "integrity": "sha512-lBh+4P2CRyspoFbBwCemTFIYmAAX8esznDsYYDGgV0+Id9z7Xi5pRZUAFY33XodYgTnMPiN4TJyfd8bfJbPNzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.7",
+ "@base-ui/utils": "^0.2.9",
+ "@mui/utils": "^9.0.1",
+ "core-js-pure": "^3.49.0",
+ "reselect": "^5.2.0",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
@@ -3989,6 +4112,17 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/core-js-pure": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz",
+ "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -4029,6 +4163,12 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
+ "node_modules/dayjs": {
+ "version": "1.11.21",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
+ "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -5277,6 +5417,12 @@
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
+ "node_modules/reselect": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
+ "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
diff --git a/package.json b/package.json
index ede93e8..97c0648 100644
--- a/package.json
+++ b/package.json
@@ -9,11 +9,13 @@
"@hello-pangea/dnd": "^18.0.1",
"@mui/icons-material": "^9.1.0",
"@mui/material": "^9.1.0",
+ "@mui/x-date-pickers": "^9.6.0",
"@preact/signals": "^2.9.1",
"@schedule-x/calendar": "^4.6.0",
"@schedule-x/events-service": "^4.6.0",
"@schedule-x/react": "^4.1.0",
"@schedule-x/theme-default": "^4.6.0",
+ "dayjs": "^1.11.21",
"fzf": "^0.5.1",
"ics": "^3.12.0",
"lz-string": "^1.5.0",
diff --git a/src/Planner.tsx b/src/Planner.tsx
index c89a5a3..3e89a41 100644
--- a/src/Planner.tsx
+++ b/src/Planner.tsx
@@ -6,6 +6,8 @@ import { createViewWeek, CalendarConfig } from "@schedule-x/calendar";
import { createEventsServicePlugin } from "@schedule-x/events-service";
import { useCalendarApp, ScheduleXCalendar } from "@schedule-x/react";
import { Temporal } from "temporal-polyfill";
+import { TimePicker } from "@mui/x-date-pickers/TimePicker";
+import dayjs from "dayjs";
import "@schedule-x/theme-default/dist/index.css";
@@ -29,12 +31,6 @@ function shortLocation(raw: string): string {
return "";
}
-function formatTime(date: Date): string {
- return `${String(date.getHours()).padStart(2, "0")}:${String(
- date.getMinutes(),
- ).padStart(2, "0")}`;
-}
-
function TimeInput({
value,
onChange,
@@ -45,18 +41,30 @@ function TimeInput({
label: string;
}) {
return (
- {
- if (!e.target.value) return;
- const [hours, minutes] = e.target.value.split(":").map(Number);
+ {
+ if (!newVal) return;
const day = new Date(value);
- day.setHours(hours, minutes, 0, 0);
+ day.setHours(newVal.hour(), newVal.minute(), 0, 0);
onChange(day);
}}
+ ampm={false}
+ slotProps={{
+ textField: {
+ size: "small",
+ slotProps: { htmlInput: { "aria-label": label } },
+ sx: {
+ width: "5.5rem",
+ "& .MuiInputBase-input": {
+ p: "4px 8px",
+ fontSize: "0.8rem",
+ textAlign: "center",
+ },
+ "& .MuiInputAdornment-root": { display: "none" },
+ },
+ },
+ }}
/>
);
}
diff --git a/src/css/planner.css b/src/css/planner.css
index ea0ee1c..bd21103 100644
--- a/src/css/planner.css
+++ b/src/css/planner.css
@@ -12,22 +12,6 @@
display: none;
}
-.planner-time-input {
- border: 1px solid lightgrey;
- border-radius: 4px;
- width: 100%;
- min-width: 0;
- max-width: 5.5rem;
- text-align: center;
- background-color: white;
- font-size: 0.875rem;
- line-height: 1.5;
-}
-
-.planner-time-input::-webkit-calendar-picker-indicator {
- display: none;
-}
-
/* custom event content */
.sx-event-title {
font-weight: 600;
diff --git a/src/index.tsx b/src/index.tsx
index d3d692f..5f2d0eb 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,5 +1,7 @@
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
+import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
+import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import App from "./App";
import "./css/tailwind.css";
@@ -10,6 +12,8 @@ const root = ReactDOM.createRoot(
root.render(
-
+
+
+
,
);