Skip to content
241 changes: 241 additions & 0 deletions chrome-extension/course_sort/course_sort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
const courseSortingLogPrefix = '[Course Sorting Script] ';

console.log(courseSortingLogPrefix + "Course Sorting script loaded");

// dropdown element
const dropdownTemplate = `
<div style="display: inline-block; margin-left: -20px; margin-right: 5px;">
<select class="size-dropdown" style="border-color: #ccc; border-radius: 3px; height: 34px; transform: translateY(3%); font-size: 14px; padding: 6px 12px;">
<option value="sortCourseCodeAsc">Sort By Course # (Default)</option>
<option value="sortSeatsAvailDes">Sort By Seats Available</option>
<option value="sortMostCommonGradeDes">Sort By A Grade Rate</option>
<option value="sortAvgGrade">Sort By Average Grade</option>
<option value="sortPopularityDes">Sort By Popularity</option>
<option value="sortUnitsDes">Sort By Units</option>
</select>
</div>
`;

// handle sorting options
const handleDropdownChange = (event) => {
var selectedValue = event.target.value;
console.log(courseSortingLogPrefix + "my choice: " + selectedValue);

const sortingFunctions = {
sortCourseCodeAsc: () => location.reload(),
sortSeatsAvailDes: () =>
{
const showSectionButtons = document.querySelectorAll('.action-sections.btn.btn-default');
showSectionButtons.forEach(button => button.click());
const loadingElement = `<h2 class='loadingIndicator'><p>Sorting...</p></h2>`;
let courseListResults = document.querySelector('.course-list-results').parentElement;
courseListResults.insertBefore(htmlToElement(loadingElement), courseListResults.firstChild);
setTimeout(() => {
sortCourses(buildSeatsAvailableDict, 'sortSeatsAvailDes');
}, 500);
},
sortMostCommonGradeDes: () => sortCourses(buildMostCommonGradeDict, 'sortMostCommonGradeDes'),
sortAvgGrade: () => sortCourses(buildCourseAvgDict, 'sortAvgGrade'),
sortPopularityDes: () => sortCourses(buildPopularityDict, 'sortPopularityDes'),
sortUnitsDes: () => sortCourses(buildUnitsDict, 'sortUnitsDes')
};

sortingFunctions[selectedValue]?.();
};

const sortCourses = (buildDictFunc, dropdownValue, additionalLogic = () => {}) => {
sortCoursesTemplate(buildDictFunc, additionalLogic).then(courseDict => {
let courseListResults = document.querySelector('.course-list-results');
let courseDivs = Array.from(courseListResults.firstElementChild.children);

courseDivs.sort((a, b) => {
let nameA = a.querySelector('a').getAttribute('name');
let nameB = b.querySelector('a').getAttribute('name');
return courseDict[nameB] - courseDict[nameA];
});

console.log(courseSortingLogPrefix + "Sorted Course Dict: ", courseDict); // For debugging purposes, display the updated dictionary
console.log(courseSortingLogPrefix + "Sorted Course Divs: ", courseDivs); // For debugging purposes, display the updated divs

courseListResults.firstElementChild.innerHTML = '';
courseDivs.forEach(courseDiv => {
courseListResults.firstElementChild.appendChild(courseDiv);
});

document.querySelector('.size-dropdown').value = dropdownValue;
}).catch(error => {
console.error(courseSortingLogPrefix + 'Error building course dictionary:', error);
});
};

const sortCoursesTemplate = (buildDictFunc, additionalLogic) => {
return buildDictFunc().then(courseDict => {
additionalLogic();
return courseDict;
});
};

let courseSizeDict = {};

const buildSeatsAvailableDict = () => {
return new Promise((resolve, reject) => {
let courseListResults = document.querySelector('.course-list-results');
let courseDivs = Array.from(courseListResults.firstElementChild.children);

if (Object.keys(courseSizeDict).length === 0) {
for (let courseDiv of courseDivs) {
let courseName = courseDiv.querySelector('a').getAttribute('name');
let targetDiv = courseDiv.querySelector('div:nth-child(2) > div:nth-child(3)');
let trElements = targetDiv.querySelectorAll('tr');

let sum = 0;
trElements.forEach(tr => {
let lastChildText = tr.lastElementChild.innerText;

// Merged extractDifference logic
let matches = lastChildText.match(/(\d+)\s+of\s+(\d+)/g);
if (matches) {
matches.forEach(match => {
let parts = match.match(/(\d+)\s+of\s+(\d+)/);
let current = parseInt(parts[1]);
let total = parseInt(parts[2]);
sum += (total - current);
});
}
});

courseSizeDict[courseName] = sum;
}
}

const hideSectionButtons = document.querySelectorAll('.action-sections.btn.btn-default.pull-right');
hideSectionButtons.forEach(button => button.click());
document.querySelector('.loadingIndicator').remove();

resolve(courseSizeDict);
});
};


let courseMostCommonGradeDict = {};
let courseAvgDict = {};
let coursePopularityDict = {};
let courseUnitsDict = {};

const buildDictionary = (fetchDataFunc, dictToUpdate) => {
const loadingElement = `<h2 class='loadingIndicator'><p>Sorting...</p></h2>`;
let courseListResults = document.querySelector('.course-list-results').parentElement;
courseListResults.insertBefore(htmlToElement(loadingElement), courseListResults.firstChild);

return new Promise((resolve, reject) => {
let courseListResults = document.querySelector('.course-list-results');
let courseDivs = Array.from(courseListResults.firstElementChild.children);
let fetchPromises = [];

if (Object.keys(dictToUpdate).length === 0) {
for (let courseDiv of courseDivs) {
let courseName = courseDiv.querySelector('a').getAttribute('name');
let fetchPromise = new Promise((resolve, reject) => {
try {
fetch(`https://umn.lol/api/class/${courseName}`)
.then(res => {
if (!res.ok) {
dictToUpdate[courseName] = 0;
}
return res.json();
}).then(response => {
if (response.success && response.data) {
let result = fetchDataFunc(response.data);
dictToUpdate[courseName] = result;
} else {
dictToUpdate[courseName] = 0;
}
resolve();
}).catch(error => {
console.error(courseSortingLogPrefix + 'Error fetching data:', error);
});
} catch {
dictToUpdate[courseName] = 0;
resolve();
}

});
fetchPromises.push(fetchPromise);
}
}

Promise.all(fetchPromises)
.then(() => {
document.querySelector('.loadingIndicator').remove();
resolve(dictToUpdate);
})
.catch(error => {
document.querySelector('.loadingIndicator').remove();
reject(error);
});
});
};


const buildMostCommonGradeDict = () => {
return buildDictionary(
data => {
let totalStudents = data.total_students;
let totalGrades = data.total_grades;
let numberOfAs = totalGrades["A"];
return (numberOfAs / totalStudents) * 100;
},
courseMostCommonGradeDict
);
};

const buildCourseAvgDict = () => {
return buildDictionary(
data => {
const gpaValues = {
"A": 4.0, "A-": 3.7, "B+": 3.3, "B": 3.0,
"B-": 2.7, "C+": 2.3, "C": 2.0, "C-": 1.7,
"D+": 1.3, "D": 1.0, "F": 0.0
};

let allGrades = data.total_grades;
let totalStudents = 0;
let totalWeightedGpa = 0;

for (let grade in allGrades) {
if (gpaValues.hasOwnProperty(grade)) {
let numberOfStudents = allGrades[grade];
let gpaValue = gpaValues[grade];
totalStudents += numberOfStudents;
totalWeightedGpa += numberOfStudents * gpaValue;
}
}

return totalWeightedGpa / totalStudents;
},
courseAvgDict
);
};

const buildPopularityDict = () => {
return buildDictionary(
data => data.total_students,
coursePopularityDict
);
};

const buildUnitsDict = () => {
return buildDictionary(
data => data.cred_min,
courseUnitsDict
);
};


function resetDictionaries() {
courseMostCommonGradeDict = {};
courseAvgDict = {};
coursePopularityDict = {};
courseUnitsDict = {};
courseSizeDict = {};
}
3 changes: 2 additions & 1 deletion chrome-extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"sidebar/sidebar.css"
],
"js": [
"sidebar/sidebar.js"
"sidebar/sidebar.js",
"course_sort/course_sort.js"
]
},
{
Expand Down
37 changes: 37 additions & 0 deletions chrome-extension/sidebar/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const htmlToElement = (html) => {
return template.content.firstChild;
};



const iframeTemplate = `
<div class="gopher-grades-container">
<iframe class="gopher-grades-result-iframe" referrerpolicy="unsafe-url"></iframe>
Expand Down Expand Up @@ -155,6 +157,19 @@ const loadCourseSchedule = (courseSchedule) => {
}
};

const loadDropdown = () => {
if ((window.location.host + window.location.pathname).startsWith('schedulebuilder.umn.edu/explore/')) {
const courseListOptions = document.querySelector(".course-list-options");
const emptyDiv = courseListOptions.firstElementChild;
const dropdownElement = htmlToElement(dropdownTemplate);
const firstDiv = emptyDiv.firstElementChild;
emptyDiv.insertBefore(dropdownElement, firstDiv);
}
};

let currentPage = 0;
let lastPage = 0;

const onAppChange = async () => {
const courseList = document.querySelector(".course-list-results");
const courseInfo = document.querySelector("#crse-info");
Expand All @@ -170,10 +185,32 @@ const onAppChange = async () => {
if (courseList) loadCourses(courseList);
else if (courseInfo) loadCourseInfo(courseInfo);
else if (courseSchedule) loadCourseSchedule(courseSchedule);

var courseSortDropdown = document.querySelector('.size-dropdown');
if (courseSortDropdown) {
courseSortDropdown.addEventListener('change', handleDropdownChange);
}
if (!courseSortDropdown) loadDropdown();

const activeItem = document.querySelector('.page-item.active').firstElementChild;
if (activeItem) {
lastPage = activeItem.getAttribute('page-value');
}
if (currentPage != lastPage) {
if (currentPage != 0 && document.querySelector('.size-dropdown').value != 'sortCourseCodeAsc') {
location.reload();
// we reload because of some weird bug that expands all sections
}
currentPage = lastPage;
resetDictionaries();
}
};



let loaded = false;
const onLoad = () => {

if (loaded) return;
loaded = true;

Expand Down
14 changes: 14 additions & 0 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ const nextConfig = {
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
reactStrictMode: true,
swcMinify: true,
async headers() {
return [
{
// matching all API routes
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: "*" },
{ key: "Access-Control-Allow-Methods", value: "GET,DELETE,PATCH,POST,PUT" },
{ key: "Access-Control-Allow-Headers", value: "Origin, X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" },
]
}
]
},
rewrites: async () => {
return [
{
Expand Down