Blackboard To-Do List

Create a To-Do List for Blackboard that shows upcoming assignments, grades, submitted status

// ==UserScript==
// @name         Blackboard To-Do List
// @namespace    http://yourwebsite.com
// @version      1.1
// @license MIT
// @description  Create a To-Do List for Blackboard that shows upcoming assignments, grades, submitted status
// @author       Your Name
// @match        https://ccccblackboard.blackboard.com/ultra/course*
// @grant        none
// ==/UserScript==

console.log('%cLoaded Brad\'s To Do List!', 'color: yellow; font-weight: bold;font-size:400%;');
function createMenu() {
    const toDoList = document.getElementById('toDoList');
    if (!toDoList) {
        const courseColumnsCurrent = document.getElementById('course-columns-current');
        courseColumnsCurrent.style.width = '75%';

        // Create a new 'toDoList' div
        const newToDoList = document.createElement('div');
        newToDoList.id = 'toDoList';
        courseColumnsCurrent.insertBefore(newToDoList, courseColumnsCurrent.firstChild);
    }

    const tdl = document.getElementById('toDoList');
    tdl.style.cssText = 'position:absolute;left:79%;top:20%;background:white;border-radius:4px;border:1px solid #cdcdcd;width:4%;height:70%;padding: 5px;overflow-y:scroll;overflow-x:hidden;';
}

async function fetchAssignments() {
    const startSearchDate = new Date().toISOString();
    const endSearchDate = new Date();
    endSearchDate.setDate(endSearchDate.getDate() + 7);
    const endSearchDateISO = endSearchDate.toISOString();

    const response = await fetch(`https://ccccblackboard.blackboard.com/learn/api/v1/calendars/calendarItems?since=${startSearchDate}&until=${endSearchDateISO}`);
    const json = await response.json();
    return json.results.map(e => ({
        class: {
            id: e.calendarId,
            name: e.calendarNameLocalizable.rawValue,
            link: `https://ccccblackboard.blackboard.com/ultra/courses/${e.calendarId}/cl/outline`
        },
        assignmentName: e.title,
        assignmentId: e.itemSourceId,
        assignmentLink: `https://ccccblackboard.blackboard.com/ultra/courses/${e.calendarId}/cl/outline?legacyUrl=~2Fwebapps~2Fcalendar~2Flaunch~2Fattempt~2F_blackboard.platform.gradebook2.GradableItem-${e.itemSourceId}`,
        assignmnetCategoryId: e.dynamicCalendarItemProps.categoryId,
        dueDate: new Date(e.endDate),
        assignedDate: new Date(e.startDate)
    }));
}

async function renderToDoList(assignments) {
    const tdl = document.getElementById('toDoList');
    tdl.innerHTML = `<h1 style='font-family: Open Sans,sans-serif;text-align:center;font-weight:600;'>To Do List</h1>`;


    tdl.innerHTML += "<img id='reloadTDL' style='cursor:pointer;position:absolute;top:3%;' src='https://learn.content.blackboardcdn.com/3900.74.0-rel.32+80961aa/images/ci/ng/small_refresh.gif'/>"

    if(localStorage.completedAssignments?.length==0) {
        localStorage.completedAssignments=JSON.stringify([]);
    }
    tdl.innerHTML+=`<h4>Complete Now</h4>`

    let addedTomorrowSpacer = false;
    for(const assignment of assignments) {
      const className = assignment.class.name.split("_")[0];
      const assName = assignment.assignmentName;
      const linkToClass = assignment.assignmentLink;
      let checked = localStorage.completedAssignments.includes(assName)
      const dueTommorrow = (assignment.dueDate - new Date()) / 1000 / 60 / 60 > 24;
      const grade = await getAssignmentGrade(assignment.class.id, assignment.assignmentId);
      const submitted = grade.toString().includes("Submitted");
      if(!checked && submitted) {
        checked = true;
      }

      if (!addedTomorrowSpacer && dueTommorrow) {
          const tomorrowSpacer = document.createElement('h4');
          tomorrowSpacer.textContent = 'Start Today, Complete Tomorrow';
          tdl.appendChild(tomorrowSpacer);
          addedTomorrowSpacer = true;
      }


      const assignmentBox = document.createElement('div');
      assignmentBox.classList.add('assignmentBox');
      assignmentBox.style.outline = '1px solid #cdcdcd';
      assignmentBox.style.width='98%'
      assignmentBox.style.margin = '8px';
      assignmentBox.style.borderRadius = '3px';
      assignmentBox.style.padding = '3px';
      assignmentBox.style.background="rgb(250,250,250)";

      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.id = assName;
      checkbox.value = 'test';
      assignmentBox.appendChild(checkbox);

      const label = document.createElement('label');
      label.htmlFor = assName;
      label.style.maxWidth='90%'

      const anchor = document.createElement('a');
      anchor.href = linkToClass;
      anchor.textContent = assName;

      label.appendChild(anchor);
      assignmentBox.appendChild(label);

      const detailsSpan = document.createElement('span');
      detailsSpan.style.color = 'gray';
      detailsSpan.style.fontSize = '90%';
      detailsSpan.innerHTML = `<br>Due ${formatDateTime(assignment.dueDate)} | ${className} ${grade}`;
      assignmentBox.appendChild(detailsSpan);

      tdl.appendChild(assignmentBox);

      checkbox.checked = checked;
      checkbox.oninput = () => {
          let completed = JSON.parse(localStorage.completedAssignments);

          if (checkbox.checked) {
              completed.push(assName)
          } else {
              completed.splice(completed.indexOf(assName), 1)
          }

          localStorage.completedAssignments = JSON.stringify(completed);
      }
      document.getElementById('reloadTDL').onclick = ()=>{
        main();
      }
  }
}

function formatDateTime(date) {
    const now = new Date();
    const tomorrow = new Date(now);
    tomorrow.setDate(tomorrow.getDate() + 1);

    const hours = date.getHours()%12;
    const minutes = date.getMinutes();

    let formattedDate;

    if (date.toDateString() === now.toDateString()) {
        formattedDate = "";
    } else if (date.toDateString() === tomorrow.toDateString()) {
        formattedDate = "tmrw";
    } else {
        const options = { month: "long", day: "numeric" };
        formattedDate = date.toLocaleDateString(undefined, options);
    }

    const formattedTime = `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}`;
    const ampm = date.getHours() >= 12 ? 'PM' : 'AM';

    return `${formattedDate} at <span style='font-weight:600;'>${formattedTime} ${ampm}</span>   ${calculateTimeDifference(now,date)}`;
}

function calculateTimeDifference(startDate, endDate) {
    const timeDifferenceMs = endDate - startDate;
    const hoursDifference = Math.floor(timeDifferenceMs / (1000 * 60 * 60));
    const minutesDifference = Math.floor((timeDifferenceMs / 60000) % 60);

    return hoursDifference >= 8 ? '' : `(${hoursDifference || minutesDifference} ${hoursDifference ? 'hrs' : 'min'})`;
}
const badgeCss = 'position:absolute;right:2%;display: inline-block;width:74px;text-align:center;border-radius:5px;padding:2px;font-size:90%;'
async function getAssignmentGrade(classId, assId) {
    let ungraded = `<span style='${badgeCss}background:#d1d1d1;color:#555;'>Incomplete</span>`;
    let submittedText = `<span style='${badgeCss}background:#95deb0;color:black;'>Submitted</span>`;

    try {
        const response = await fetch(`https://ccccblackboard.blackboard.com/learn/api/v1/courses/${classId}/gradebook/grades?limit=100&userId=${window.__initialContext.user.id}`);
        const content = await response.json();
        const result = content.results.find(a => a.columnId === assId);
        return result ? doColorGrade(result.displayGrade.score) : ungraded;
    } catch (e) {
        try{
          const response = await fetch(`https://ccccblackboard.blackboard.com/webapps/calendar/launch/attempt/_blackboard.platform.gradebook2.GradableItem-${assId}`);
          const content = await response.text();
          const submitted = content.includes("Review Submission History");
          return submitted ? submittedText : ungraded;
        } catch(a) {
          return ungraded;
        }
    }
}
function doColorGrade(grade) {
  let color = "#ff3b30";
  if(grade>=90) color="#39e379";
  else if(grade>=80) color="#cdee4b";
  else if(grade>=70) color='#ffe300';
  else if(grade>=60) color='#ff9600';

  return `<span style='${badgeCss}background:${color};color:#000;'>${grade.toFixed(2)+"%"}</span>`;
}
let loadedMenu = false;
setInterval(()=>{
  if(document.getElementsByClassName('element-details summary').length<1 || location.href!="https://ccccblackboard.blackboard.com/ultra/course") {
    loadedMenu = false;
    return;
  }

  if(location.href=="https://ccccblackboard.blackboard.com/ultra/course" && !loadedMenu) {
    loadedMenu = true;
    main();
  }
},100)

async function main() {
    createMenu();
    const assignments = await fetchAssignments();

    const sortedAssignments = assignments.filter(assignment => {
        const hours = (assignment.dueDate - new Date()) / 1000 / 60 / 60;
        return hours < 48 && hours > 0;
    }).sort((a, b) => (a.dueDate - b.dueDate) / 1000 / 60 / 60);

    renderToDoList(sortedAssignments);
}