The Instructure Community will enter a read-only state on November 22, 2025 as we prepare to migrate to our new Community platform in early December.
Read our blog post for more info about this change.
Found this content helpful? Log in or sign up to leave a like!
UI for weekly progression idea that rest in the Schedule tab of the
**K-12 UI** of Canvas
it's incomplete and I really would not take it and push it into instance but I am going to post it here because in the past people like @James && @robotcars || @Steve_25 have had terrific input in the past...
maybe w/their eyes and knowledge this can actually be worthwhile to others...
-The screenshot depicts what it looks like
2 points here:
-the font is set by the user
and
-the theme is the dark mode for us also st by the user
The colored bars depict the progression, assignments submitted, not the grade, for the week selected.
The percentages and the bars are simply the Number of assignments due that week vs what was submitted presented as a percentage `Submitted Assignments / Total Assignments) * 100 = Completion Percentage`
I tried to make use of the users localStorage so that the list of assignments are not fetched all the time just on an interval that you set default for us is daily. the submissions are on interaction or on click
and that is about it..
The original idea sprang up when I saw this useless bar on the grades page that corresponded to a grade corresponds is actually to strong of a term actually... but here is a screenshot of that
finally here is the code:
async function assGlance() {
const checkIfNull = async (selector) => {
while (document.querySelector(selector) === null) {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
return document.querySelector(selector);
};
checkIfNull("#dashboard-app-container").then(() => {
console.log('start');
function setWithExpiry(value, ttl) {
const now = new Date()
// object
// expire
const item = {
value: [value],
expiry: now.getTime() + ttl,
}
localStorage
.setItem('savedAssCol', JSON.stringify(item))
};
function getWithExpiry() {
const itemStr = localStorage.getItem('savedAssCol')
// if the object doesn't exist, return null
if (!itemStr) {
return null
}
const item = JSON
.parse(itemStr);
const now = new Date();
// compare the expiry time of the item with the current time
if (now.getTime() > item.expiry) {
// If the object is expired, throw it out and restock
// and return null
localStorage
.removeItem('savedAssCol');
fetchAssignmentsInParallel();
}
return item.value
};
const getMonday = (date) => {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
// Adjust day: 0 (Sunday) to -6, 1 (Monday) to 1, etc.
const day = d.getDay();
const diff = d.getDate() - day + (day === 0 ? -6 : 1);
return new Date(d.setDate(diff));
};
const getShownDay = () => {
try {
// Use a more robust selector or context
const dateText = document.querySelector('#dashboard_page_schedule .PlannerApp [data-testid="day"] h2 > div')?.textContent;
if (!dateText) return null;
// Append the current year to the month/day string for a valid date
const fullDateStr = `${dateText} ${new Date().getFullYear()}`;
const date = new Date(fullDateStr);
// Check if date is valid
return date instanceof Date && !isNaN(date) ? date : null;
} catch (error) {
console.error("Error getting shown day:", error);
return null;
}
};
//console.log("shown: " + getShownDay());
//console.log("Mon: " + getMonday(new Date()));
let weekToShow = () => {
if (!getShownDay() || getShownDay() === null) {
return getMonday(new Date());
} else {
return new Date(getShownDay());
}
};
//console.log("weekToShow" + weekToShow());
const currentCourses = (window.ENV?.STUDENT_PLANNER_COURSES || [])
.filter(enr => enr.enrollmentType === "StudentEnrollment" && enr.enrollmentState === "active");
class returnedData { // For the final data shown in the UI
constructor(courseId, courseName, courseLink, courseColor, assCount, subCount, unSubCount) {
Object.assign(this, { courseId, courseName, courseLink, courseColor, assCount, subCount, unSubCount });
}
}
class savedAssExtension { // For the cached assignment list (all assignments for a course)
constructor(courseId, courseName, courseLink, courseColor, ass) {
Object.assign(this, { courseId, courseName, courseLink, courseColor, ass });
}
}
class savedAss { // Individual assignment object structure
constructor(due_at, id, html_url, workflow_state) {
Object.assign(this, { due_at, id, html_url, workflow_state });
}
}
//let assAll = [];
const weekSelectedAss = [];
const colorCollection = ['Sienna', 'RebeccaPurple', 'LightSeaGreen', 'Coral', 'Azure', 'FireBrick', 'GoldenRod', 'Indigo', 'LightPink'];
let finalCollection = [];
const savedAssCol = getWithExpiry() || [];
function filterChop(cId, cName, cLink, cColor, fullAssignmentList) {
// Calculate the week range once
const weekStart = weekToShow();
// Use setDate to efficiently calculate the end of the week (Monday + 5 days, so end is Sat/Sun)
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 5); // Up to 6 days after Monday (i.e., Sunday)
// Filter assignments based on the due date range
const dataFilter = fullAssignmentList
.filter(assignment => {
const dueDate = new Date(assignment.due_at);
// Due date is greater than or equal to Monday AND less than Sunday's start
return dueDate >= weekStart && dueDate < weekEnd && assignment.workflow_state === "published";
});
if (dataFilter.length > 0) {
// Only store the IDs of the assignments needed for the submission fetch
const assignmentIds = dataFilter.map(obj => obj.id.toString());
weekSelectedAss.push(new savedAssExtension(cId, cName, cLink, cColor, assignmentIds));
}
};
async function genUI(weekSomething, useRemove) {
console.log('Generating UI');
let inSchedule = window.location.href.includes("#schedule");
const targetContainer = "#dashboard-app-container .flexCardProgress";
const insertBeforeSelector = "#dashboard_page_schedule";
// --- 1. Preparation and Removal ---
// Find the existing container to remove/replace
const existingContainer = document.querySelector(targetContainer);
//console.log('--------------------------------------');console.log(existingContainer === null);console.log(inSchedule);console.log(useRemove);
if (!inSchedule && existingContainer !== null) {
existingContainer.style.display = 'none';
return;
}
if (useRemove && existingContainer !== null && inSchedule) {
// Remove the old element faster
existingContainer.remove();
//console.log('Removed existing progress container.');
}
if (!useRemove && existingContainer !== null && inSchedule) {
// Remove the old element faster
existingContainer.remove();
//console.log('Removed existing progress container.');
}
// Create the new element container
const newContainer = document.createElement('span');
newContainer.classList.add('flexCardProgress');
// Use a DocumentFragment to build the content outside the DOM
const fragment = document.createDocumentFragment();
// Start with the header and pacMan
let contentHTML = '<h3 style="margin:0">Weekly Assignment Progress</h3><div class="gameLoader pacMan"></div>';
// --- 2. Build Content ---
if (!weekSomething
||
weekSomething.length
===
0) {
contentHTML += `<p>Nothing to show for the selected week. I'll return again one day... </p>`;
} else {
const courseContent = weekSomething
.map((course, i) => {
// (Submitted Assignments / Total Assignments) * 100 = Completion Percentage
const compPerc = course.assCount > 0 ? (course.subCount / course.assCount) * 100 : 0;
const roundedPerc = parseInt(compPerc);
const color = course.courseColor
||
`linear-gradient(to right, ${colorCollection[Math.floor(Math.random() * colorCollection.length)]} 0%, ${colorCollection[Math.floor(Math.random() * colorCollection.length)]} 100%)`;
// Ensure consistent color property setting
document.documentElement.style.setProperty(`--course${i}`, color);
// Return HTML string for this course
return `
<a href="${course.courseLink}" style="font-size:1em;text-decoration:none;cursor:pointer;">
${course.courseName.substring(0, 40)}
<span>${roundedPerc}%</span>
</a>
<progress class="courseProgression course${i}" max="100" value="${compPerc}" role="listitem"
aria-label="${course.courseName}, ${roundedPerc}% complete of 100%">
</progress>
`;
})
.join(''); // stringOrgy
contentHTML += courseContent;
}
// Set the HTML content of the new container (big money operation)
newContainer.innerHTML = contentHTML;
// Append the fully constructed container to the fragment
fragment.appendChild(newContainer);
// --- 3. Ass Insertion ---
const insertTarget = document.querySelector(insertBeforeSelector);
if (insertTarget) {
insertTarget.before(fragment);
} else {
console.error(`Insertion target element (${insertBeforeSelector}) not found.`);
}
// Stop pacMan
if (useRemove) {
document.querySelector('.gameLoader.pacMan')?.style.setProperty("--playState", "paused");
}
};
async function subMissionsInParallel(filterResult) {
//console.log('Fetching submissions in parallel:', filterResult);
// Clear the global collection once at the start
finalCollection.length = 0;
// Check if there's any data to process
if (filterResult.length === 0) {
return;
}
// 1. Map the filterResult array to an array of Promises
const fetchPromises = filterResult
.filter(s => s.ass.length > 0) // Only include courses with assignments
.map(async (s) => {
const courseId = s.courseId;
const assignmentIds = s.ass;
// Build the query string for all assignment IDs in this course
const query = assignmentIds
.map(id => `assignment_ids[]=${id}`).join('&');
try {
let options = {'headers': {'accept': 'application/json'},'timeout': 5000};
const response = await fetch(`/api/v1/courses/${courseId}/students/submissions?${query}&per_page=100`, options);
if (!response.ok) { // Throw to be caught by the local catch block
throw new Error(`HTTP error! Status: ${response.status
} for course ${courseId}`);
}
const data = await response.json();
// If the response is empty, skip processing
if (data.length === 0) {
return null;
}
// 2. Process the submission data
let submittedCount = 0;
let unsubmittedCount = 0;
for (const sub of data) {
//console.log(courseId + sub.workflow_state)
if (sub.workflow_state !== "unsubmitted" && sub.missing !== true) {
submittedCount++;
} else {
unsubmittedCount++;
}
}
// 3. Return the processed progress data for this course
// This will be collected by Promise.all, hopefully
return new returnedData(courseId, s.courseName, s.courseLink, s.courseColor, assignmentIds.length, submittedCount, unsubmittedCount);
} catch (error) {
console.error(`Error fetching submissions for course ${courseId}:`, error);
return null; // Return null so Promise.all doesn't fail and we can filter errors later
}
});
// 4. Wait for ALL fetch operations to complete concurrently
const results = await Promise.all(fetchPromises);
// 5. Pop finalCollection
// Filter out null results (error or empty response)
const successfulResults = results.filter(result => result !== null);
finalCollection.push(...successfulResults);
};
async function weekClicked() {
console.log('weekClicked');
const tbody = document.querySelector("#dashboard_page_schedule > div.PlannerApp");
const homePage = document.querySelector("#tab-tab-homeroom");
// Use a flag to prevent rapid, multiple executions if the kid manipulates the DOM changes quickly
let isProcessing = false;
if (!tbody || !homePage) {
console.warn("Target not found.");
return;
}
// If we're already processing, ignore the new mutation event.
if (isProcessing) {
return;
}
const doTheRightThing = async () => {
//console.log('has mutants');
isProcessing = true; // Set flag to true to block subsequent calls
if (savedAssCol
===
null
&&
savedAssCol
!==
'[]') {
fetchAssignmentsInParallel(); //fetchAssignmentsSequentially()
isProcessing = false; // Set flag to false cause its done *beep*
} else {
let savedAssColData = getWithExpiry();
let savedAssColDataParsed = JSON.parse(savedAssColData);
weekSelectedAss.length = 0; //mp-t the array
finalCollection.length = 0; //mp-t the array
//console.log("weeSelected arrar " + weekSelectedAss.length);
//loading animation
document.querySelector('.gameLoader.pacMan').style.setProperty("--playState", "play");
//loading animation
if (savedAssColDataParsed
!== null
&& savedAssColDataParsed
!== undefined
&& savedAssColDataParsed.length
> 0) {
for (const course of savedAssColDataParsed) {
//console.log(course);
if (course.ass.length > 0) {
filterChop(course.courseId, course.courseName, course.courseLink, course.courseColor, course.ass);
}
}
};
//console.log(weekSelectedAss);
await subMissionsInParallel(weekSelectedAss);
await genUI(finalCollection, true);
isProcessing = false; // Set flag to false cause its done *beep*
}
};
async function handleMutationsA(mutationList) {
//console.log("A " + mutationList);
const childList = mutationList.some(mutation => mutation.type === 'childList');
const plannerApp = mutationList.some(mutation => mutation?.target?.classList.contains('PlannerApp'));
if ((childList && plannerApp)) {
//console.log(`top: ${(childList && plannerApp)}`);
doTheRightThing();
}
else{
return;
}
};
async function handleMutationsB(mutationList) {
//console.log("B " + handleMutationsB);
const attList = mutationList.some(mutation => mutation.type === 'attributes');
const scheduleTab = mutationList.some(mutation => mutation?.target?.id === 'tab-tab-schedule');
const ariSelected = mutationList.some(mutation => mutation?.target?.hasAttribute('aria-selected'));
if ((attList && scheduleTab && ariSelected)) {
//console.log(`bottom: ${(attList && scheduleTab && ariSelected)}`);
if (document.querySelector("#tab-tab-schedule").hasAttribute('aria-selected')) {
const statusValue = document.querySelector("#tab-tab-schedule").getAttribute('aria-selected');
if (statusValue === 'true') {
//console.log(statusValue);
doTheRightThing();
}
else{
return;
}
}
} else {
return;
}
}
let config = (theThing)=>{
if (theThing == 'a') {
return { childList: true };
} if (theThing == 'b') {
return { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-selected'] };
}
};
const observerA = new MutationObserver(handleMutationsA);
const observerB = new MutationObserver(handleMutationsB);
observerA.observe(tbody, config('a'));
observerB.observe(document.querySelector('#dashboard-app-container [role="tablist"]'), config('b'));
};
const nextURL = (linkTxt) => {
if (linkTxt) {
let links = linkTxt.split(",");
let nextRegEx = new RegExp('^<(.*)>; rel="next"$');
for (let i = 0; i < links.length; i++) {
let matches = nextRegEx.exec(links[i]);
if (matches && matches[1]) {
//return right away
//console.log(matches[1]);
return matches[1];
}
}
} else {
return false;
}
};
const loopy = async (response, assignmentCollection) => {
const linkTxt = response.headers.get("Link");
if (!linkTxt) return assignmentCollection;
const nextURLMatch = linkTxt.match(/<([^>]+)>; rel="next"/);
if (!nextURLMatch || !nextURLMatch[1]) return assignmentCollection;
const nextUrl = nextURLMatch[1];
try {
const options = {credentials: "same-origin",headers:{accept: "application/json"},timeout: 5000};
const rspns = await fetch(nextUrl, options);
if (!rspns.ok) {
console.warn(`Pagination failed for ${nextUrl}. Status: ${rspns.status}`);
return assignmentCollection; // Return current data if next page fails
}
const moreData = await rspns.json();
assignmentCollection.push(...moreData);
// Recursive call with the new response
return await loopy(rspns, assignmentCollection);
} catch (e) {
console.error(`Error during pagination loop for ${nextUrl}:`, e);
return assignmentCollection; // Return current data on fetch error
}
};
async function fetchAssignmentsInParallel() {
console.log('Fetching assignments in parallel');
// 1. Create an array of Promises for all fetch operations
const fetchPromises = currentCourses
.map(async (course) => {
try {
const options = {'headers': {'accept': 'application/json',},'timeout': 5000,};
const url = `/api/v1/users/self/courses/${course.id}/assignments/?order_by=due_at&per_page=100`;
// but keeping the original structure simple.
const response = await fetch(url, options);
const responseJson = await response.json(); // Await parsing
const responseObj = {ok:response.ok,headers: response.headers};
if (responseObj.ok) {
let assignmentCollection = responseJson;
assignmentCollection = await loopy(responseObj,assignmentCollection);
//console.log(assignmentCollection);
let assAll = new Array();
assignmentCollection.forEach(d => {
assAll.push(new savedAss(d.due_at, d.id, d.html_url, d.workflow_state));//due_at, html_url, workflow_state
});
//console.log(assAll);
// Return an object containing the course details and assignment data
// if the fetch was successful and there is data.
if (responseJson.length > 0) {
return {
courseId: course.id,
shortName: course.shortName,
href: course.href,
color: course.color,
data: assAll,
};
}
return null; // Return null if no data to be filtered out later
} else{
// Throw an error to be caught by the outer try/catch
throw new Error(`HTTP error! Status: ${response.status} for course ${course.id}`);
}
} catch (error) {
console.error(`Error fetching assignments for course ${course.id}:`, error);
return null; // Return NULL on failure so Promise.all can still resolve is null a dattype in JS?
}
});
// 2. Wait for all Promises
const results = await Promise.all(fetchPromises);
// 3. Process the results
// Clear the global arrays before populating them to ensure fresh minty flavor
savedAssCol.length = 0;
weekSelectedAss.length = 0;
results
.filter(result => result !== null)
.forEach(result => {
const { courseId, shortName, href, color, data }
= result;
// Populate savedAssCol
savedAssCol.push(new savedAssExtension(courseId, shortName, href, color, data));
// Filter for the current week
filterChop(courseId, shortName, href, color, data);
});
// 4. Update the local storage cache once
if (savedAssCol.length > 0) {
setWithExpiry(JSON.stringify(savedAssCol), 43200000);//EXPIRE_BY
}
return weekSelectedAss;
}
//gate way drugs are best in parallel
fetchAssignmentsInParallel()
.then(async (filterResult) => {
try {
await subMissionsInParallel(filterResult);
} catch (error) {
console.error("The .catch() block handled an error:", error);
} finally { // return weekSomething = finalCollection ;
await genUI(finalCollection, false)
.then(() => {
weekClicked();
})
}
})
})};
assGlance();
The css components are as follows:
you don't need the pacman loader it is the one we use to differentiate homegrown from instructure
un-comment the flexcard section to enable a glow that circulates around the edges
/*
__
||
====
| |__
| |-.\
|| \\
course ||
======__|
________||__
/____________\
*/
.course1::-webkit-progress-value {
background:var(--course1);
}
.course2::-webkit-progress-value {
background:var(--course2);
}
.course3::-webkit-progress-value {
background:var(--course3);
}
.course4::-webkit-progress-value {
background:var(--course4);
}
.course5::-webkit-progress-value {
background:var(--course5);
}
.course6::-webkit-progress-value {
background:var(--course6);
}
.course7::-webkit-progress-value {
background:var(--course7);
}
.course8::-webkit-progress-value {
background:var(--course8);
}
.course9::-webkit-progress-value {
background:var(--course9);
}
.course0::-webkit-progress-value {
background:var(--course0);
}
.courseProgression {
padding:0;
margin: 0;
-webkit-appearance:none;
-moz-appearance:none;
appearance:none;
border:none;
background-size:auto;
height:20px;
width: calc(var(--card-progress-width) - 3rem);
}
.flexCardProgress {
padding:0;
margin: 0;
background: #f0f0f0;
width: var(--card-progress-width);
padding: .5rem 1rem;
margin: .5rem 0;
position: relative;
/* border-radius: 6px; */
display: flex;
flex-direction: column;
font-size: 1em;
color: #000;
}
/* .flexCardProgress::after {
position: absolute;
content: "";
top: calc(var(--card-progress-height) -1);
left: 0;
right: 0;
z-index: -1;
height: 100%;
width: 100%;
margin: 0 auto;
filter: blur(calc(var(--card-progress-height) / 6));
background-image: linear-gradient(
var(--rotate-progress)
, #5ddcff, #3c67e3 43%, #4e00c2);
opacity: 1;
transition: opacity .5s;
animation: spin 20.5s linear infinite;
}
@keyframes spin {
0% {
--rotate-progress: 0deg;
}
100% {
--rotate-progress: 360deg;
}
} */
.flexCardProgress a {
padding:0;
margin: 0;
color: #191c29;
text-decoration: none;
font-weight: bold;
margin-top: .25rem;
cursor: pointer;
}
.flexCardProgress span{
margin: 0;
display: flex;
color: #191c29;
width: 8rem;
padding-right: 1rem;
position: absolute;
left: calc(100% - 4rem);
--bottom: 15px;
}
.flexCardProgress div{
margin: 0;
display: flex;
width: 4px;
position: absolute;
left: calc(100% - 4rem);
top: 1.5rem;
}
/*
_ __ __ _ ___ _ __ ___ __ _ _ __
| '_ \ / _` |/ __| '_ ` _ \ / _` | '_ \
| |_) | (_| | (__| | | | | | (_| | | | |
| .__/ \__,_|\___|_| |_| |_|\__,_|_| |_|
| |
|_|
*/
.gameLoader {
width: 15px;
height: 15px;
position: relative;
display: inline-block;
margin-left: 7px;
}
.gameLoader::before,
.gameLoader::after {
content: "";
position: absolute;
}
.pacMan {
border-radius: 50%;
width: 4px;
height: 4px;
-webkit-animation-name: pacmanDot;
animation-name: pacmanDot;
-webkit-transform: translateX(14px);
transform: translateX(14px);
}
.pacMan::before,
.pacMan::after {
border-radius: 50%;
border: 14px solid #005299;
border-right-color: transparent;
top: -12px;
left: -24px;
}
.pacMan,
.pacMan::before,
.pacMan::after {
-webkit-animation-duration: 0.5s;
animation-duration: 0.5s;
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
animation-play-state: var(--playState, paused);
}
.pacMan::before {
-webkit-animation-name: upperJaw;
animation-name: upperJaw;
}
.pacMan::after {
-webkit-animation-name: lowerJaw;
animation-name: lowerJaw;
}
@-webkit-keyframes pacmanDot {
0%,
50% {
background: #008000;
}
51%,
100% {
background: none;
}
0%,
100% {
-webkit-transform: translateX(19px);
transform: translateX(19px);
}
50% {
-webkit-transform: translateX(12px);
transform: translateX(12px);
}
}
@keyframes pacmanDot {
0%,
50% {
background: #008000;
}
51%,
100% {
background: none;
}
0%,
100% {
-webkit-transform: translateX(19px);
transform: translateX(19px);
}
50% {
-webkit-transform: translateX(12px);
transform: translateX(12px);
}
}
@-webkit-keyframes upperJaw {
50% {
-webkit-transform: rotate(50deg) translate(2px, -2px);
transform: rotate(50deg) translate(2px, -2px);
}
}
@keyframes upperJaw {
50% {
-webkit-transform: rotate(50deg) translate(2px, -2px);
transform: rotate(50deg) translate(2px, -2px);
}
}
@-webkit-keyframes lowerJaw {
50% {
-webkit-transform: rotate(-50deg) translate(2px, 2px);
transform: rotate(-50deg) translate(2px, 2px);
}
}
@keyframes lowerJaw {
50% {
-webkit-transform: rotate(-50deg) translate(2px, 2px);
transform: rotate(-50deg) translate(2px, 2px);
}
}
Community helpTo interact with Panda Bot, our automated chatbot, you need to sign up or log in:
Sign inTo interact with Panda Bot, our automated chatbot, you need to sign up or log in:
Sign in