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 f56d6ee..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"; @@ -13,10 +15,20 @@ import "./css/planner.css"; const hasWeekendCourse = false; -function formatTime(date: Date): string { - return `${String(date.getHours()).padStart(2, "0")}:${String( - date.getMinutes(), - ).padStart(2, "0")}`; +/** 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: "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)); + if (buildings.length > 0) return buildings[0]; + return ""; } function TimeInput({ @@ -29,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" }, + }, + }, + }} /> ); } @@ -66,7 +90,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 +128,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..bd21103 100644 --- a/src/css/planner.css +++ b/src/css/planner.css @@ -12,18 +12,20 @@ display: none; } -.planner-time-input { - border: 1px solid lightgrey; - border-radius: 4px; - width: 100%; - min-width: 0; - max-width: 4.5rem; - text-align: center; - background-color: white; - font-size: 0.875rem; - line-height: 1.5; +/* custom event content */ +.sx-event-title { + font-weight: 600; + font-size: 0.75rem; + line-height: 1.2; } -.planner-time-input::-webkit-calendar-picker-indicator { +.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/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( - + + + , ); 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;