diff --git a/chrome-extension/calendar/calendar.css b/chrome-extension/calendar/calendar.css
index 78657cf..2dcb2ed 100644
--- a/chrome-extension/calendar/calendar.css
+++ b/chrome-extension/calendar/calendar.css
@@ -31,12 +31,38 @@
display: flex;
justify-content: space-between;
align-items: center;
+ position: relative; /* Needed for tooltip positioning */
}
#gg_gcal_button:hover {
opacity: 90%;
}
+.gg_gcal_tooltip {
+ visibility: hidden;
+ opacity: 0.7;
+ background-color: #5B0013;
+ box-shadow: #5B0013 0 0 14px;
+ color: #fff;
+ text-align: center;
+ border-radius: 4px;
+ padding: 4px 8px;
+ position: absolute;
+ left: 50%;
+ top: 40px;
+ transform: translateX(-50%);
+ z-index: 10000;
+ font-size: 13px;
+ transition: opacity 0.2s;
+ pointer-events: none;
+ white-space: nowrap;
+}
+
+#gg_gcal_button:hover .gg_gcal_tooltip {
+ visibility: visible;
+ opacity: 1;
+}
+
#cover-spin {
position: fixed;
width: 100%;
diff --git a/chrome-extension/calendar/calendar.js b/chrome-extension/calendar/calendar.js
index 4687b7a..20fb2b3 100644
--- a/chrome-extension/calendar/calendar.js
+++ b/chrome-extension/calendar/calendar.js
@@ -5,6 +5,7 @@ const buttonTemplate = `
`;
diff --git a/chrome-extension/calendar/utils/scraper.js b/chrome-extension/calendar/utils/scraper.js
index 6e5300f..ca59652 100644
--- a/chrome-extension/calendar/utils/scraper.js
+++ b/chrome-extension/calendar/utils/scraper.js
@@ -1,9 +1,23 @@
/**
+ * # Course Data Scraper Utilities
+ *
* Some utilityish functions to scrape course data
* The two functions work, but this alone is not yet very useful
* Also the functions are still missing some features, (see commented-out lines in JSON objects)
+ *
+ * Main Functions:
+ * - weekToJSON: Scrapes meetings for a given week
+ * - generalClassInfo: Scrapes general info for a class
+ * - scraperSecondPass: Cross-references and enhances course/meeting info
+ * - scrapeASemester: Orchestrates scraping for an entire semester
+ *
+ * ORGANIZED: The file is restructured, with original comments retained where useful for context.
*/
+// ============================
+// Week-Level Scraping
+// ============================
+
/**
* Scrapes a list of json objects containing all the meetings in a given week
* Json includes course name, number, date (a single date!), time, room, etc.
@@ -17,7 +31,7 @@ const weekToJSON = async (dateString = "today") => {
// appends the date info to our base url
let baseURL =
- "https://www.myu.umn.edu/psp/psprd/EMPLOYEE/CAMP/s/WEBLIB_IS_DS.ISCRIPT1.FieldFormula.IScript_DrawSection?group=UM_SSS§ion=UM_SSS_ACAD_SCHEDULE&pslnk=1&cmd=smartnav"; // the base url
+ "https://www.myu.umn.edu/psp/psprd/EMPLOYEE/CAMP/s/WEBLIB_IS_DS.ISCRIPT1.FieldFormula.IScript_DrawSection?group=UM_SSS§ion=UM_SSS_ACAD_SCHEDULE&pslnk=1&cmd=smartnav";
let url = baseURL.concat("&effdt=", dateString);
// create a (queryable!) DOM element from the url
@@ -27,48 +41,107 @@ const weekToJSON = async (dateString = "today") => {
.then((r) => (el.innerHTML = r));
// parsing begins
- var synchronousMeetings = el.querySelector(".myu_calendar"); // HTML div containing only the classes with set days and times
+ var synchronousMeetings = el.querySelector(".myu_calendar");
+ console.log("[GG DEBUG] synchronousMeetings:", synchronousMeetings);
+
if (synchronousMeetings == null) {
- console.log(`[GG] encountered a week without meetings (${dateString}).`);
+ console.log(`[GG DEBUG] encountered a week without meetings (${dateString}).`);
return {
meetingObjects: [],
sundayDate: sundayThisWeek(parseDate(dateString, "yyyy-mm-dd")),
};
}
- var meetingElements = synchronousMeetings.querySelectorAll(
- ".myu_calendar-class"
- ); // list of all the classes this week as HTML elems
- const meetingObjects = []; // list of json objects holding meeting data
+
+ var meetingElements = synchronousMeetings.querySelectorAll(".myu_calendar-class");
+ const meetingObjects = [];
+
+ // Loop through each calendar class element
for (let meetingEl of meetingElements) {
+ console.log("[GG DEBUG] Found meetingEl:", meetingEl, meetingEl.innerText);
+
+ // Skip any placeholder for days with no classes
if (meetingEl.classList.contains("no-class")) {
- // sometimes a meetingEl marks when there are no classes in a day?
- console.log("[GG] encountered a no-class meetingElement. skipping...");
+ console.log("[GG DEBUG] encountered a no-class meetingElement. skipping...");
continue;
}
-
- let classDetails = meetingEl
- .querySelector(".myu_calendar-class-details")
- .innerHTML.replace(/\n/g, "") // get rid of random newlines that are in there for some reason
+
+ // Defensive: Try to get class details element
+ let classDetailsEl = meetingEl.querySelector(".myu_calendar-class-details");
+ if (!classDetailsEl) {
+ console.warn("[GG DEBUG] No .myu_calendar-class-details found for:", meetingEl);
+ continue;
+ }
+ let classDetailsRaw = classDetailsEl.innerHTML;
+
+ // Clean up HTML to plain text
+ let classDetails = classDetailsRaw
+ .replace(/\n/g, "")
.replace(/ /g, "\n");
- let courseTitleScrape = meetingEl.querySelector(
- ".myu_calendar-class-name"
- ).innerText;
-
- meetingObjects.push({
- term: meetingEl.getAttribute("data-strm").trim(), // {string} // in format `xyyx', where `yy` is the year and `xx` = `13` is spring, `19` is fall
- courseNum: meetingEl.getAttribute("data-class-nbr").trim(), // {string}
- date: parseDate(meetingEl.getAttribute("data-fulldate"), "yyyymmdd"), // {Date}
- meetingType: classDetails.match(/^(Lecture)|(Discussion)|(Laboratory)$/m)[0], // {string} // may need updating if list is not exhaustive
- timeRange: classDetails.match(/.*:.*/m)[0], // {string}
- room: classDetails.match(/^ .* $/m)[0].trim(), // {string} // (room has leading and trailing space)
- courseName: meetingEl
- .querySelector(".myu_calendar-class-name-color-referencer")
- .innerText.trim(), // {string}
- institution: meetingEl.dataset.institution, // {string}
- prettyName: courseTitleScrape.match(/(?<=\d\) ).*/)[0].trim(), // {string} e.g. "Adv Programming Principles"
- sectionID: courseTitleScrape.match(/(?<=\()\d+(?=\))/)[0], // {string} e.g. "001" of "CSCI 2021 (001)"
- }); // {MeetingObject} spec
+
+ // Defensive: Try to get course title element
+ let courseTitleEl = meetingEl.querySelector(".myu_calendar-class-name");
+ if (!courseTitleEl) {
+ console.warn("[GG DEBUG] No .myu_calendar-class-name found for:", meetingEl);
+ continue;
+ }
+ let courseTitleScrape = courseTitleEl.innerText;
+
+ // Defensive: Try to get color referencer
+ let courseNameEl = meetingEl.querySelector(".myu_calendar-class-name-color-referencer");
+ let courseName = courseNameEl ? courseNameEl.innerText.trim() : null;
+
+ // Extract with regex, with fallback if match fails
+ let meetingTypeMatch = classDetails.match(/^(Lecture|Discussion|Laboratory)/m);
+ let timeRangeMatch = classDetails.match(/.*:.*/m);
+ let roomMatch = classDetails.match(/(\n\s*.+\s*\n)/m); // More robust for room line
+
+ let prettyNameMatch = courseTitleScrape.match(/(?<=\d\) ).*/);
+ let sectionIDMatch = courseTitleScrape.match(/(?<=\()\d+(?=\))/);
+
+ // Print extracted pieces
+ // console.log("[GG DEBUG] meetingTypeMatch:", meetingTypeMatch);
+ // console.log("[GG DEBUG] timeRangeMatch:", timeRangeMatch);
+ // console.log("[GG DEBUG] roomMatch:", roomMatch);
+ // console.log("[GG DEBUG] prettyNameMatch:", prettyNameMatch);
+ // console.log("[GG DEBUG] sectionIDMatch:", sectionIDMatch);
+
+ // Defensive: Data attributes
+ let term = meetingEl.getAttribute("data-strm");
+ let courseNum = meetingEl.getAttribute("data-class-nbr");
+ let dateRaw = meetingEl.getAttribute("data-fulldate");
+ let institution = meetingEl.getAttribute("data-institution");
+
+ // Defensive: Only add if all required data is present
+ if (
+ term && courseNum && dateRaw &&
+ meetingTypeMatch && timeRangeMatch && roomMatch &&
+ courseName && institution && prettyNameMatch && sectionIDMatch
+ ) {
+ // Parse date string as needed
+ let date = parseDate(dateRaw, "yyyymmdd");
+
+ meetingObjects.push({
+ term: term.trim(),
+ courseNum: courseNum.trim(),
+ date: date,
+ meetingType: meetingTypeMatch[0],
+ timeRange: timeRangeMatch[0],
+ room: roomMatch[1].trim(),
+ courseName: courseName,
+ institution: institution,
+ prettyName: prettyNameMatch[0].trim(),
+ sectionID: sectionIDMatch[0],
+ });
+
+ console.log("[GG DEBUG] Added meetingObject:", meetingObjects[meetingObjects.length - 1]);
+ } else {
+ console.warn("[GG DEBUG] Skipping meeting due to missing data:", {
+ term, courseNum, dateRaw, meetingTypeMatch, timeRangeMatch, roomMatch, courseName, institution, prettyNameMatch, sectionIDMatch
+ });
+ }
}
+
+ console.log("[GG DEBUG] Final meetingObjects array:", meetingObjects);
weekObject = {
meetingObjects: meetingObjects,
@@ -77,6 +150,10 @@ const weekToJSON = async (dateString = "today") => {
return weekObject;
};
+// ============================
+// General Class Info Scraping
+// ============================
+
/**
* Scrapes general info on a class: session start, days of week, meeting times, location, etc
* @param {string} term
@@ -111,12 +188,12 @@ const generalClassInfo = async (term, courseNum, institution = "UMNTC") => {
synchronousRow = row;
synchronousRowCounter++;
} else {
- console.log("[GG] found asynchronous meeting in `generalClassInfo`, ignoring...");
+ console.log("[GG DEBUG] found asynchronous meeting in `generalClassInfo`, ignoring...");
}
}
if (synchronousRowCounter != 1) {
- console.log(`[GG]!!!!!!!!!!!!!!! ALERT, ${synchronousRowCounter} synchronous meeting found in generalClassInfo() for url ${url}`);
+ console.log(`[GG DEBUG]!!!!!!!!!!!!!!! ALERT, ${synchronousRowCounter} synchronous meeting found in generalClassInfo() for url ${url}`);
} // if this runs, that means we have a big headache and rewrite ahead of us. pls no
// dateRange parsing
@@ -148,6 +225,10 @@ const generalClassInfo = async (term, courseNum, institution = "UMNTC") => {
}; // {CourseObject} spec
};
+// ============================
+// Data Enhancement and Cross-Reference
+// ============================
+
/**
* Enhances `coursesInfo` by cross-referencing between it and `weeks`.
* Populates `meetingType`, `courseName` (but not `address` yet)
@@ -170,6 +251,7 @@ const scraperSecondPass = (weeks, coursesInfo) => {
course.sectionID = meeting.sectionID;
course.prettyName = meeting.prettyName;
continue courseloop; // hah, this is fun!
+ //that's not such fun actually
}
}
}
@@ -189,23 +271,18 @@ const scraperSecondPass = (weeks, coursesInfo) => {
*/
const timeRangeRepr = (timeRangeString, format) => {
let ARBITRARY_DATE = new Date();
+ let timeRange;
if (format == "week") {
- // "hh:mm - hh:mm pm" (first am/pm is ommitted)
- timeRange = getTimes(timeRangeString, ARBITRARY_DATE); // long string that is dumb. we will cut it down >:)
+ timeRange = getTimes(timeRangeString, ARBITRARY_DATE);
} else if (format == "coursesInfo") {
- // "hh:mm pm - hh:mm pm"
- timeRange = recurringGetTimes(timeRangeString, ARBITRARY_DATE); // long string that is dumb
+ timeRange = recurringGetTimes(timeRangeString, ARBITRARY_DATE);
} else {
- throw new Error(
- `timeRangeRepr() was passed unrecognized format ${format}`
- );
+ throw new Error(`timeRangeRepr() was passed unrecognized format ${format}`);
}
return timeRange.map((s) => s.slice(9, 13)).join("-"); // crop off extraneous info
};
- // ###################################
// First, set up model week hash table
- // ###################################
let modelWeekHT = {}; // empty json object (hash table)
for (day = 0; day < 7; day++) {
// for day in a week
@@ -227,9 +304,7 @@ const scraperSecondPass = (weeks, coursesInfo) => {
let additionalMeetings = [];
- // ###############################
// Main loop through all the weeks
- // ###############################
for (let week of weeks) {
// Create a hash table for the actual week as well
let thisWeekHT = {};
@@ -254,8 +329,8 @@ const scraperSecondPass = (weeks, coursesInfo) => {
excludedDate.setDate(excludedDate.getDate() + dayOfWeek); // then shift the date to the correct day of week
// console.log(excludedDate)
- console.log(`[GG] Excluded meeting found! hash: ${hash}`);
- console.log(`[GG] date: ${excludedDate}`)
+ console.log(`[GG DEBUG] Excluded meeting found! hash: ${hash}`);
+ console.log(`[GG DEBUG] date: ${excludedDate}`)
modelWeekHT[hash].course.excludedDates.push(excludedDate);
}
}
@@ -274,6 +349,7 @@ const scraperSecondPass = (weeks, coursesInfo) => {
}
}
+ // console.log("[GG DEBUG] modelWeek: \n")
// console.log(modelWeek)
return additionalMeetings;
};
@@ -285,6 +361,10 @@ const scraperSecondPass = (weeks, coursesInfo) => {
return additionalMeetings;
};
+// ============================
+// Semester-Level Scraping
+// ============================
+
// BEGIN ZONE OF EXTRA JANK
// Scraping up all meeting times, including catching when classes are canceled for holidays
@@ -292,7 +372,7 @@ const scraperSecondPass = (weeks, coursesInfo) => {
// 1. pick a sample week (which week best to pick?), then grab all the course numbers in it
// 2. Then get the general course info for each of those course numbers, store it somewhere
// 3. Then take one of the `dateRange`s (they're all the same) and scrape through the whole thing to find instances of when a class should appear but it doesn't. store this somehow
-console.log("[GG] scraper.js runs!");
+console.log("[GG DEBUG] scraper.js runs!");
/**
* Given a date, returns info on each course meeting in the semester containing that date. Also returns generally-true info about the courses in the semester
@@ -347,4 +427,4 @@ const scrapeASemester = async (sampleDateString = "today") => {
coursesInfo: coursesInfo,
additionalMeetings: additionalMeetings,
}; // {Array}
-};
+};
\ No newline at end of file
diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json
index e0ed348..471586c 100644
--- a/chrome-extension/manifest.json
+++ b/chrome-extension/manifest.json
@@ -1,7 +1,7 @@
{
"name": "Gopher Grades - Past grades for UMN classes!",
"description": "Now you can view directly from the course catalog!",
- "version": "2.1.0",
+ "version": "2.2.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js"